How to properly use Optional to cleanse your Java code of NullPointerExceptions
Since the creation of null
in the 1960s by Tony Hoare, null
or some form of it whether it be nil
or None
, has crept its way into many languages today as a convenient way to signify a non-existent object. In doing so, it has caused problems from making developers pull their hair out trying to track down a null
reference, to even taking systems in production down.
What’s the problem with null
?
Take for example the following snippet:
Excluding the DAO being null, there are three points of failure just from null in this snippet:
- Since
Long
is not a primitive,homeListingId
is allowed to be null. A call togetExpensiveHome(null)
will successfully type check in Java which results inhomeListingDAO.get(null)
to either throw an exception or returnnull
. homeListing
could now benull
if the id isn’t in the database resulting inhomeListing.getPrice()
throwing anotherNullPointerException
.homeListing
could also not have a price yet, in which caselistingPrice
is nownull
which results inlistingPrice > EXPENSIVE_HOME_THRESHOLD
to throw aNullPointerException
.
In fact, this method doesn’t make the client’s life any easier either. From just looking at the method signature, the client doesn’t know if the returned Home
is null
or not. Even in the event that the given home price surpasses the EXPENSIVE_HOME_THRESHOLD
, homeListing.getHome()
can still return null
. That’s… a lot of places it can fail… how do we fix it?
We can nullify these problems with rewriting this a little with some simple null checks!
Ta-da! By switching from the wrapper Long
class to the primitive long
, we’ve made it so that isExpensiveHome(null)
won’t type check with the compiler before this code ever runs. Moreover, we fixed all the other null issues that were pointed out above.
While it works, this isn’t the most elegant solution. The actual business logic that the code is doing is straightforward. But, by introducing all these null
checks, we’re fighting Java’s inability to give us compile time guarantees about the types and are forced to deal with them at run time instead. As a programmer using a statically typed language, the presence of null
subverts the advantages that a type checker is supposed to give us as any Object
in Java could just be a null
reference.
Save me! What is Optional and how can we use it?
From the Oracle Java docs:
A container object which may or may not contain a non-null value. If a value is present,
isPresent()
will returntrue
andget()
will return the value.
Another way to reason about Optional
is that Optional
is like a List
but instead of having an indefinite number of elements, it can only contain at most one element. It can be either empty represented by Optional.empty()
or filled with an Object
.
Sweet, let’s take a stab using Optional
then!
This… doesn’t look much better. It just looks like we’re wrapping the result of things we know are going to return null
in Optional
. This just adds unnecessary complexity to the code! Optional
sucks! I’m going back to my null
checks!
Don’t go just quite yet! Like Java 8 Streams you can also use your favorite higher order functions,map
and filter
.
How does this help us?
To see how these operations can be useful in the context of Optional
, let’s take a look at their implementations. Let’s look at map
first:
public Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent()) {
return Optional.empty();
} else {
return Optional.ofNullable(mapper.apply(value));
}
}
This is great! This snippet here is effectively doing the null
check for us, and if the value is not null
, then the function will be applied and a new Optional
will be returned which may or may not be empty
. If it is empty, any subsequent map
operations will keep on returning Optional.empty()
and not throw any errors.
Now let’s look at filter:
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent()) {
return Optional.empty();
} else {
return predicate.test(value) ? this : empty();
}
}
It’s the same thing here! If the value isn’t present, then the function passed in will never be applied. Otherwise, the test predicate is applied on the value inside of the Optional
and if it succeeds, it will return itself, otherwise the empty optional.
Using these functions and what we know about Optional
, we can rewrite the method neatly and null-safe as such:
This above snippet accomplishes a lot in such few lines! Not only is the business logic more clear from the absence of null
checks, but now as a client of this method, it is easy to see that getExpensiveHome
has the possibility to return no Home
. This now forces the client to deal with the scenario where no Home
exists which is far better than having an NullPointerException
(NPE)!
Psh this seems like a contrived example. Show me more code!
The snippet below demonstrates retrieving a list of HomePhotos
from a given propertyId
. In this example, there can be multiple HomeListing
s per property and it will choose the first one to get the photos.
Yuck! Four null
checks makes this really difficult to read. Moreover, the filtering out of low quality photos makes this a great candidate for using the Stream
API. Let’s see how Optional
and Streams
are used in conjunction with one another:
Is this really doing the same thing as the original snippet?
You bet! First, the propertyId
is ensured to not be null
. Then, propertyRoDAO::getHomeListings
fetches the List<HomeListing>
and is converted to a Stream<HomeListing>
. The findFirst
method is called on the Stream
.
Notice that instead of calling the standard map
, we’re calling flatMap
instead. In this context, findFirst
returns an Optional<HomeListing>
. If we used map
here, we’d be left with an Optional<Optional<HomeListing>>
. To extract the HomeListing
out of the inner Optional
, we’d have to do an isPresent
check with a filter
to ensure that the HomeListing
is there and then map
Optional::get
to get the HomeListing
so that we’re back to our original type of Optional<HomeListing>
. flatMap
does this all in one step.
After a HomeListing
is fetched, the List<HomePhoto>
is now retrieved, filtered for non null
and for only high quality photos. The Stream
is collected back up into a List
and now we’re left with the type Optional<List<HomePhoto>>
.
As a general rule, it’s good practice to not wrap container types such as Streams
and Collections
in Optional
but instead to return the empty collection instead to remove the unnecessary burden for the client to unwrap the Optional
to get to the container. With that in mind, rather than returning an Optional<List<HomePhoto>>
, we’ll go a step further and use the Optional::orElse
method which will return the underlyingList<HomePhoto>
, or return the default value passed in if not present, in this case, the empty ArrayList<HomePhoto>
.
Hurray for no null!
All of this sounds great… but can’t Optional be null too?
Unfortunately because Optional
is not actually a primitive, it has the ability to be null
as well effectively defeating all of the advantages of using Optional
in the first place. It’s ultimately up to the developers and perhaps even static analysis tools to ensure that null
doesn’t get returned from a method that has a return type of Optional
. Even though there might be no guarantee on whether a given method that returnsOptional
will be null
or not, it’s still safe to construct an Optional
locally to take advantage of map
, filter
, and flatMap
to reduce the amount of manual null
checking that has be done.
Optional is great, but how do I deal with null in other languages?
Today, programming languages vary in their ways of dealing with the billion dollar mistake.
Scala and F# have taken a similar approach to Java and have their own form of Optional
not much different than the API that Java provides. In Kotlin, the type system actually makes a distinction between references that can hold null values and those that can’t denoted using a ?
that’s appended at the end of the type (ex: String?
denotes a String
that can be null
where as String
with no ?
cannot be) giving developers compile time guarantees about the scope of their null
references. Both C# and Kotlin have a variant of the null propagation operator which enables safe chaining of calling methods on objects, e.g:
obj?.getObjTwo()?.doSomething();
is the same as:
if (obj != null) {
if (obj.getObjTwo() != null) {
obj.getObjTwo().doSomething();
}
}
Unfortunately, for many dynamically typed languages, since the type checker runs at run time rather than compile time, when code is executed, there are no guarantees about the types of all the references, rendering a solution like Optional
to be ineffective. Ruby has taken steps to include the null propagation operator to avoid null
checks and even Python has a draft in the works to introduce safe navigation as well.
For some languages, null
isn’t even a problem. In a handful of functional programming languages such as Haskell, there isn’t even the concept of a null
value, developers are forced to use the equivalent to Optional
to denote that there’s a possibility for the given value to not be there.
Closing Thoughts
null
has become a staple in many languages, and it’s not going away anytime soon. Despite the fact that null
still causes major issues in production systems today, it’s reassuring to see that developers are actively trying to find ways to introduce features to better mitigate the holes that these type systems have.
So remember, the next time you have to fix an NPE in production, remember there are solutions out there rather than doing the manual null checks!