Some notes on null-intolerant Java
Null-intolerant Java is Java in which there are no internal null checks. No public method in a null-intolerant library ever returns null, and no public method will ever accept a null parameter. Nulls may very occasionally be used within a class, as part of its internal state management, but should never leak into or out of its public API.
There are a couple of flavours of null-intolerance. The first is defensive null-intolerance, in which all inbound parameters are null-checked. A caller who submits a null where one is not wanted will get a NullPointerException thrown directly from the target method, identifying the offending parameter. This makes for public API methods that look a bit like this (using Guava’s Preconditions):
Because we know that none of the values assigned to any of the fields of Person is ever null, we can forgo null-checking in other methods (such as equals), and can guarantee that methods exposing field values (such as getName) will never return null.
Another flavour of null-intolerance is carefree null-intolerance, where we don’t bother to null-check inbound values but simply assert that any client who calls any of our methods passing in null deserves everything they get (i.e. NullPointerException thrown from unpredictable locations). This may seem like an irresponsible approach to take; however, it is arguably valid for libraries whose clients are all themselves null-intolerant and can guarantee that they will never do anything so stupid as to pass in a null. In theory, it is perfectly safe for a defensively null-intolerant library to make use internally of a carefree null-intolerant library. However, this does entail the risk that a chink in the armour of the “outer” library will result in NullPointerExceptions being thrown from unpredictable locations in the “inner” library.
Fortunately, we can enlist the help of our IDE and build tools in closing the gaps, for example using Intellij’s @NotNull annotation, or the checker framework’s @NonNull, to mark parameters that do not accept null values and methods that guarantee never to return them. Eschewing defensive null-intolerance, but making use of code inspections or compile-time annotation processing to ensure compliance with null-freedom is verified null-intolerance, since in place of defensive programming techniques we are using automated verification methods to keep unwanted nullable values at bay.
(While we’re on the subject, a minor beef I have with Kotlin is that its compiled code performs runtime null-checks on all non-nullable parameters at the start of every public method invocation, presumably in case any of these methods is being accessed from a language without Kotlin’s compile-time null-checking. While the performance impact is mostly nugatory, it would still be nice to be able to turn this off.)
It may sometimes be appropriate to use a combination of techniques. For example, verified null-intolerance will not protect objects which are instantiated using reflection (e.g. by your favourite ORM or data format deserialiser) from being contaminated with nulls. You can use JSR-303 annotations and a validator to check that bean-like objects fulfil the contract defined for them after they have been instantiated, or you can force instantiators to use factory methods that perform manual checking before instantiating (or, you can check for nulls inside the constructor. I consider this bad style, because I prefer “clean” constructors that literally do nothing besides calling superconstructors and assigning values to internal fields, but YMMV).
Why should you write null-intolerant code, in whatever style? The main reason is that code that does not have to perform ubiquitous null checks is easier to read and reason about; it will generally have a lower cyclomatic complexity, as there will simply be less branching logic. Potentially missing values can be explicitly indicated using Optionals: everything not Optional is compulsory. That said, I have seen inexperienced teams promptly convert every field and parameter to an Optional, and replace null checks with Optional.isPresent(), which if anything makes things worse. You have to begin from a position of clarity about your data model: which values are required, and which are not? Can you group values which are only required in particular circumstances (in which case they are all required) together?
A particular pitfall here is the use of mutable bean-like objects, i.e. those initialised with all fields empty and subsequently populated using “setter” methods. Use of this pattern essentially forces every reference-type field to be nullable: replacing null with Optional.empty() and having every “getter” return an Optional replicates the (pathologically bad) pattern and adds extra syntactic noise to boot. Null-intolerant code will as a rule require that every object be instantiated in a valid state; it doesn’t enforce immutability of value objects, but strongly supports it.
Users of Jackson should be familiar with the use of the @JsonCreator annotation, which supports reflective deserialisation straight into a constructor or factory method and eliminates the need for “setters”. Users of Hibernate can use constructor expressions in HQL queries, but Hibernate’s “entity bean” model is the way it is fundamentally because of Hibernate’s cacheing behaviour, which may be something your application needs. It therefore makes sense to treat it as an external API and wrap a null-intolerant layer around it.
What about the builder pattern? Here it makes sense to allow nullable internal fields (as the builder piecewise accumulates the data required to instantiate an object), but to forbid passing in null values to the builder’s “setter” or “with” methods, and to perform a completeness check before assembling the built object. Nulls neither enter nor leave the builder’s public API (since a builder has no “getter”s), but are used internally to represent temporarily unfilled values. There is no advantage to using Optionals purely internally.
Finally, when handling return values from external libraries that are not null-intolerant, there is a choice to be made between performing null checks directly on returned value and wrapping them using Optional.ofNullable (which converts null to Optional.empty()) and then using Optional.orElse(), Optional.orElseGet() and Optional.orElseThrow() to supply default values or throw appropriate exceptions. For example, given a Map lookup which will return null if there is no value at the given key, compare:
The version using Optional.ofNullable() isn’t obviously superior to the version which directly null-checks the return value (in fact, it’s less efficient, because it constructs an instance of Optional only to immediately throw it away). If, however, we want to leave it to the method’s caller to decide what to if there is no value for the key, rather than throwing an exception immediately, then we should use Optional.ofNullable() to return an Optional for the caller to work with.