Skip to content

Instantly share code, notes, and snippets.

Last active February 28, 2018 23:23
Show Gist options
  • Save throughnothing/4a14dd3e4ab5e68eb154123ad11dae79 to your computer and use it in GitHub Desktop.
Save throughnothing/4a14dd3e4ab5e68eb154123ad11dae79 to your computer and use it in GitHub Desktop.
Java 8 Coding Guide / Principles

Java 8 Coding Guide

This coding guide was written for a Java 8 project, but contains principles that are valid in many languages. It is aimed at producing code that is easy to reason about, easy to validate and test, easy to maintain, and easy to avoid common bugs and pitfalls.

Remember that code is read many more times than it is written! We want to optimize for readability, comprehension, and reduced complexity!

Avoid explicit null (or, Don't use null)

Null is famously the "billion dollar mistake" (and that's probably an under-estimation!). Don't use null as a way of representing error or failure, and also don't use null as a way to avoid passing a certain argument to a function. Essentially you should never explicitly use a null value unless you are checking that something is not null. Even in those cases, it should probably be handled better by an Option<T>.


Expressions over statements

Expressions are declarative and evaluate to (and return) a value, while statements are imperative bits of code that perform actions. Because of this, expressions are easier to reason about, debug, and verify correctness for.

A simple example is an if statement, vs an if expression. If in java does not return a value, nor does java enforce that an if have an else branch. Additionally, if there is an else branch, it does not have to result in the same type of value as the if branch. This makes if statements hard to reason about and understand. The ternary form predicate ? value : value is preferable to if/then statements.

In a language like Java, statements will be required, but we should isolate those to their own, small, methods, and keep their surface area as small as possible to reduce risk and complexity from these parts of the code.


Avoid explicit loops

With Java 8, we have streams, and should no longer need explicit loops. Loops are very repetetive, and error prone, so we should leverage abstractions that handle looping for us to avoid bugs, verbosity, and complexity in our code.

There may be some cases where loops are more desirable (possibly for performance), but they should be avoided until it is clear they are actually needed.


Use immutable data structures

We should leverage guava's immutable data structures as much as possible to avoid complicated bugs related to mutating state.

Additionally, we should leverage Lombok's @Value objects as much as possible to make our own data objects immutable.


Don’t throw exceptions - use values!

In general, we should never throw an exception of our own. Obviously we will use libraries that throw exceptions. These exceptions should be caught directly in the function/method using the library, and turned into values (to be returned) as much as possible.

This is because exceptions are like goto statements, which make the flow of your program much more difficult to reason about. Every function that throws an exception can return a value in 2 ways, through the "normal" return flow, or through an exception, and every user of that function has to think about both cases. It is generally easier to reason about functions that only return values (something like vavr's Either or Try types work very well for this). This also makes it much easier to leverage Java 8's lambda-using utilities, as they don't like accepting Functions that throw exceptions.


Fail early, enforce invariants

It is always best to fail as early as possible, at the soonest point that you find something is wrong. This often means that we should validate all of our data up front, to decouple validation from the business logic that needs to use the values in question.

Additionally, the more we can make fail at compile time, rather than run time, the better off we will be at finding bugs earlie. Use Java's typesystem as much as you can to enforce invariants statically!

For our own data objects, we should have strong validation of each object, so that once we've constructed one, we know it will have good data in the rest of our program. Failed construction (on validation errors) should also be expressed by returning values, rather than exceptions. Something like vavr's Validation type is useful for this purpose. Make it impossible to construct a "bad"/invalid data object!


Composition over Inheritance

Inheritance can be very fragile, and make refactoring of complex code involving inheritance hierarchies very difficult to do safely and confidently. Composition (and in java, Interfaces) should be favored over inheritence where possible.

Often inheritance is required by certain frameworks / libraries, but we should limit the depth of inheritance hierarchies that we use as much as possible.

Composition, via composing interfaces or composable types, allows for more design flexibility, and easier, more confident refactoring.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment