Better Null-Handling With Java Optionals
Until Java 8, there was no better way to handle null
references than checking your variables at every turn.
The new class java.util.Optional<T>
changed that significantly.
But first, some history and comparison with other languages.
Table of Contents
A Billion-Dollar Mistake
The inventor of the null reference apologized in 2009 for its creation:
I call it my billion-dollar mistake. It was the invention of the null reference in 1965.
At that time, I was designing the first comprehensive type system for references in an object-oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement.
This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
– Sir Charles Antony Richard Hoare at QCon London in 2009
Programming languages are dealing very differently with the mistake.
Many of them have a proper way to handle null
, directly integrated into the language itself.
Groovy, Swift, Ruby, Kotlin, C#, and more support a safe navigation operator: .?
, or &.
in the case of Ruby.
Other languages, like Objective-C, or Clojure, decided to ingrain better null handling. Time for some different approaches to null handling.
How do other languages handle null?
Swift
In Swift, the null
reference is called nil
.
The language has excellent support for optional types and nil
handling.
With its help, we can safely call properties, and even chain them fluently together:
In combination with the null coalescing operator, we can make it even better and don’t have to deal with an optional type:
Clojure
This article is about Java, so let us look at an example from another JVM language.
Clojure also prefers nil
for the null
reference.
But instead of providing a safe navigation operator to work around the problem, Clojure decided to try to get rid of the NullPointerException
entirely.
Even though nil
is kind of Java’s null
, it’s just another value type.
Only one instance of nil
exists, and it tests to false
.
It’s still possible to encounter a NullPointerException
, but it’s way harder if you use Clojure the way it’s intended.
With nil
being a value representing nothingness instead of being nothing, it’s way easier to handle and incorporate into your code.
Objective-C
Being a Smalltalk-inspired language, Objective-C doesn’t call methods or fields, it sends messages to objects.
And sending a message to nil
will not raise an exception, the message will be discarded silently.
This can be great, you don’t have to null
-check everything.
But it’s also bad because you might not realize that a message wasn’t answered.
Java Optionals
With the release of Java 8, a straightforward way of handling null
references was provided in the form of a new class: java.util.Optional<T>
.
It’s a thin wrapper around other objects, so you can interact fluently with the Optional
instead of a null
reference directly if the contained object is not present.
Creating Optionals
Optionals are created in different ways:
Every use-case has its own reason for existence:
Case 1: We know/need a value
Even though Optionals are a great way to deal with NullPointerExceptions
, what if we actually need one?
Just because the API design uses Optionals, there might be a need to throw an exception in specific cases.
That’s what Optional#of(...)
is for, which doesn’t accept null
-values.
Case 2: We’re not sure
The most used form of creation.
No null check is performed.
Case 3: We’re sure there’s no valueOptional.empty()
returns a static final
field.
No need to create a new Optional every time it’s empty, and we know about it for sure.
Checking for values
The easiest way to check is Optional#isPresent()
:
To not check against false
all the time, Java 11 introduced Optional#isEmpty()
.
Get the actual value
Only checking if a value is present or not is not much of a help, we want to use it! The simplest way is Optional#get()
:
Congratulations, we’ve just thrown an exception!
Even though it wasn’t a NullPointerException
, there must be a better way than checking isPresent()
every time we use Optionals.
A Better Way
In real code, we don’t just want to get the inner value. Instead, we want to work with it. Thanks to lambda expressions, we can do this easily and lazily.
Before we got Optionals, the usual way to handle values was something like this:
With Optionals, we can compact this code into an easily-readable single-method call chain:
Optional#map(...)
will only be called if the Optional actually contains a value, if not, Optional#orElse(...)
is called.
Null Coalescing
Java doesn’t support a null-coalescing operator like other languages:
With Optionals, we can use the same pattern by using Optional#orElse(...)
without any additional actions in between:
Actions and Alternatives
The methods of java.util.Optional
can roughly be separated into two groups: actions and alternatives.
Actions
map(Function<? super T,? extends U> mapper)
If a value is present, the mapper function will be applied, and a new Optional with the result will be returned. Otherwise, an empty Optional will be returned.flatMap(Function<? super T,Optional<U>> mapper)
Likemap(...)
, but can handle Optionals itself.filter(Predicate<? super T> predicate
)
If a value is present and matches the predicate, the Optional is returned. Otherwise, an empty Optional will be returned.ifPresent(Consumer<? super T> consumer)
The consumer will only be invoked if a value is present.
Alternatives
Methods providing an alternative in the case of no value starting with or
:
orElse(T other)
If no value is present,other
is returned.orElseGet(Supplier<? extends T> other)
A lazy way to get an alternative value if none are present in the Optional.orElseThrow(Supplier<? extends X> exceptionSupplier)
If no value is present, invoke a supplier for an exception, e.g.,orElseThrow(IllegalArgumentException::new)
.
Primitive Data Types
With java.util.Optional<T>
being a generic type, how should we handle primitive data types like int
or long
?
Of course, primitives can never be null
, but that’s not necessarily a good thing.
Being able to represent nothingness, compared to not-initialized/default value, can be an advantage.
One way to deal with primitives is not using them and relying on their corresponding object wrapper class, e.g., int -> Integer
. But this will only produce overhead from auto-boxing/unboxing.
Until Project Valhalla, with its improved value type handling, is available to us, we can use specialized Optional classes to use the power of Optionals with primitive data types:
There are still some primitive data types missing, but it’s a start. Also, they lack all action methods, except ifPresent(LongConsumer consumer)
.
Caveat
Optionals are just Java objects like any other object, so they might be null
themselves.
If we design an API and decide to use Optionals, we must not return null
for an Optional, always use Optional.empty()
.
This has to be enforced by convention, the compiler can’t help us out, except with additional tooling and @NonNull
annotations.
Other standard Java features that we shouldn’t use directly with Optionals are about equality: equals(...)
and hashCode()
are first about comparing the Optional, and only second about the value.
The documentation warns us that the results might be unpredictable.
Conclusion
Optionals are a great way to handle null
references in a more concise way.
We can replace many if-else
constructs and even incorporate lambda expressions.
Instead of using null
, we should design our APIs with Optionals, if possible and applicable.
Even primitives are usable to a certain extent, so there are no more excuses for using Optionals in our next project.
Resources
- Null pointer (Wikipedia)
- java.util.Optionals (Oracle)
- Java 8 Optionals (Oracle)
- Java 8 in Action (Book)
- Valhalla (OpenJDK)