After 5 years without posting anything, I’m reviving this “blog” a bit with a bunch of heterogeneous posts about best practices. Let’s be honest, this will be a bad of things without much coherence to it, but my goal has not change: have a record of my opinions on best practices that survives across the companies I work for.
Today’s post is the first of a short series on various best practices. You can find the full list of articles in this series using the best-practices-series tag. Most of them were originally aimed at Java, though they are generally applicable to more languages. Where relevant, I will mention when some of them are not in one of the other languages I care about. More specifically, this article will deal with healthy handling of nullability in a codebase.
Let’s dive right in!
Dealing with null/missing values
If you’ve written Java code in a codebase which I’d deem unhealthy in that regard, you probably dread experiencing an NPE - NullPointerException - in your
application, resulting from forgetting to handle a possibly null variable. I have been in codebases in which it was very difficult to trace whether a
value could be null or not, and as a result you often had to handle the possibility of null, without knowing whether it was needed or not. This pollutes the code,
especially when it’s recurrent and in cases for which there’s no clear answer for what should happen when the value is missing.
However, by following simple hygiene, one can simplify the code, keep it concise, and almost completely avoid having NPEs. The general take-away is:
- almost don’t ever use
null. The convention should be that a variable is non-nullable by default, and nullability is an exception. - if you need nullable variables, reduce their scope as much as possible and document their nullability with a type or an annotation.
Let’s take a look at this in more details.
Constructor parameters
If you are using Lombok in your project, which I warmly recommend, always use the @NonNull annotation except for primitive types in constructors. This will generate a runtime nullity check, contrarily to javax’s @NonNull which is purely informative.
Even a purely informative annotation is useful if you’re not using Lombok, but in that case I’d recommend considering everything that is unannotated is non-nullable, and only treat nullable parameters in a special way. This approach reduces boilerplate
and gets everyone used to the idea that nullability is the exception, not the rule. Note that if your constructor is also generated by Lombok, you can still put the @NonNull annotation, but directly on class attributes, which will have the same effect on the generated constructor.
If a parameter is nullable, prefer using Optional. Editors like IntelliJ will issue a warning saying this should be used only as a return type, but I disagree. It’s useful whenever something is nullable. Many functional languages do not have null, and instead
represent missing values with a monad like Option, Scala’s cousin of Java’s Optional. Worth noting that some languages do even better by making missing values be a type of its own (not different from using a monad in that regard), but also have little impact on the syntax.
This is the case in languages such as Kotlin or Typescript, where nullable types are represented with a question mark (e.g. String?), and a few simple operators are provided to deal with the absence of such values. But I digress, back to Java.
When you prefer not to use Optional, use @Nullable instead. This is also a purely informative tag and contrarily to Optional, which allows the compiler to help you deal with missing values at compile-time. Still, annotating nullable values is a good way to remind people to handle them
appropriately, particularly in a codebase where nullable values are in extreme minority (which they should be). This approach is more acceptable for constructors than for methods, as we’ll see below.
Ok, all these rules are nice and pretty, but why am I making these recommendations specifically about constructors? Well, classes are generally instantiated much less often than the instantiated instance is used. This seems logical. If you are instantiating a class, calling a single method and then
throwing the instance away, it means you probably just need a static method instead. The implication of that is that it’s valuable to validate constructor parameters well, so that later use of these parameters (if they become class attributes) by the class’ methods (or other classes if the attributes are exposed)
can rely on certain properties, such as not being nullable! Check it once and for all, and then rest assured that you never have to check again… if you keep class attributes final that is, which you should in general as seen in the article about immutability.
This is not only true for nullability. It’s often useful to carry this type of validation in constructors so that once you hold an instance of the class, you can rely on some invariants about it and its attributes.
Non-static class members
If the constructor is generated by Lombok, add @NonNull on all non-nullable fields except primitive types. If you choose to represent a missing value as null and it needs to be exposed,
mark it as Nullable and create a getter that exposes it as Optional nonetheless. It’s generally more acceptable to have a nullable private variable than a public one, because the closer
your code is to the place that owns the variable, the easier it is to know that something is nullable or not, and the more knowledge the surrounding code has about how the variable is populated.
For code outside though, you want to make the contract as clear and safe as possible, which Optional achieves, expressing both the intent to return a possibly missing value, and also allowing
compile-time type safety.
Method parameters
By default, all parameters should be considered non-nullable, so we do not have to annotate them. The difference with constructors is that having validation in constructors is useful because the data passed to the constructor might not be used before some time (deferred usage). It can also be true for method parameters if it’s passed around through many layers of code, but experience shows that we don’t need to have null checks everywhere to avoid NPEs, so why clutter the code with them?
If a parameter is nullable, prefer using Optional. If using a nullable value, always annotate with @Nullable. This is mostly acceptable for internal methods, that are typically private. The smaller the scope, the better, because the code that is close to this method has better knowledge of its logic. The wider scope this method has, the more it becomes dodgy and Optional becomes a strong recommendation.
Return types
If the return value is non-nullable, don’t bother annotating it for the same reasons as for method parameters. Once you have a codebase where 95% of variables are non-null (which includes Optional values), you won’t feel the need to specify when a value cannot be missing.
If the return value is nullable, Optional is strongly preferred in that case. A regular return type annotated with @Nullable is also acceptable if the method has a small scope,. The wider access is given to this method, the more it’s important to lean towards using Optional.
In tests
Testing what happens when a null value is passed to a non-nullable field is never required in my opinion, unless you want a codebase full of low-value, high-boilerplate code. Especially if you use Lombok in constructors or in method parameters which you really really don’t want to be null, the null checks are automated, further reducing the need to write tests for it. For nullable parameters, testing both with a null and non-null values is mandatory if you want to cover all the branches of your class or methods public contract. And you should want that!
This concludes this first part, that was a short article with some amount of duplication, but I wanted to slightly nuance the guidelines for each situation, because not all these situations have the same impact on maintainability and readability.