Avoid Runtime Errors with Optionals
Coding Aug 19, 2019
Null is bad for representing the absence of a value, use Optionals instead. Use Optional.flatMap to chain multiple operations that might not return a value.
The problem with null 🔗
Let's recap what null
is and what problem it entails.
The language Java allows every non-primitive variable to take the value null
. If a variable holds the value null
, then all method calls to it will fail with a NullPointerException
at runtime. The compiler can only give limited hints if a method call on a variable is safe. Therefore, the programmer needs to make assumptions if a method call is safe. If they are wrong with their assumptions, NullPointerExceptions
might occur during runtime, interrupting the control flow, even possibly taking down the application. If the programmer is over-cautious, then the code is riddled with (necessary and unnecessary) null
-checks.
Why use null anyway? 🔗
In many programs, a null
value is used to indicate the absence of a value. This is a less noisy option than to signalize each absence of a value with an exception. In some cases, a null
value is used to indicate that a variable has not been initialized yet. Some use cases might even assign the null
value of a variable a dedicated meaning.
To motivate the usage of Optionals
instead of nulls
, let's first take a look at an example using null
to represent the absence of a value. After that, we solve the example in a better way with Optionals
.
Modeling with null 🔗
Let's consider an interface Person
. It contains a method getMiddleName
to retrieve the middle name of the person. Not every person has a middle name, therefore this method returns null
when the person does not have a middle name.
interface Person {
public String getMiddleName();
}
Additionally, we define a function to retrieve a person by id. This function fetches a person from a data storage. In case no person with the given ID exists, the function returns null
.
public static Person findPerson(int id) { /* … */ }
Now assume we want to write a function returning the middle name of a person by the ID of the person.
public static String getPersonMiddleName(int id) {
return findPerson(id).getMiddleName();
}
We fetch a person by its ID and then return its middle name. Unfortunately, this naive implementation is erroneous although it compiles without errors. It will return with a NullPointerException
in case no person with the given ID exists. To fix this, we can return null
from getPersonMiddleName
if findPerson
returned null. This entails that the caller of getPersonMiddleName
can not determine the reason for the result being null
. For our use case, we consider this to be fine.
Note The
Either
type goes a step further than theOptional
type and can fix this.
Trying to solve the NullPointerException
without Optionals
, we are required to inspect the return value of findPerson
and differentiate between a null
return value and a non-null
return value.
public static String getPersonMiddleName(int id) {
Person person = findPerson(id);
if (person == null) {
return null;
}
return person.getMiddleName();
}
Now, the function can not result in a NullPointerException
anymore. But it requires three additional lines of null
handling and the compiler did not support us in preventing the NullPointerException
in the first place.
Refining with Optional 🔗
Let's take a look at the alternative approach with Optionals
.
First, we change the method getMiddleName
to explicitly state that it might return an absent value.
interface Person {
public Optional<String> getMiddleName();
}
Now, the method returns either the middle name in an Optional
or an empty Optional
. It will by contract never return a null
value. Second, we make the same change to findPerson
.
public static Optional<Person> findPerson(int id) { /* … */ }
A call to findPerson
always returns a non-null Optional
. It either contains the found person or it is empty. Let's build the function getPersonMiddleName
. As we did with the null
solution, let's first implement it in the naive way.
public static Optional<String> getPersonMiddleName(int id) {
return findPerson(id).getMiddleName();
}
This time, the compiler will fail because the types do not match. The expression findPerson(id)
resolves to an instance of Optional<Person>
and therefore getMiddleName()
can not be called on the return value. Instead of running into an issue at runtime, we have caught the mistake already at compile time. We can use Optional.flatMap
to define what should happen in case findPerson(id)
leads to a result. The method flatMap
in the context of Optionals
automatically handles the case where the Optional
is empty and will return an empty Optional
in that case.
public static Optional<String> getPersonMiddleName(int id) {
return findPerson(id)
.flatMap(person -> person.getMiddleName());
}
If findPerson(id)
resolves to an empty Optional
, then the lambda person -> person.getMiddleName()
won't be called and getPersonMiddleName
will return an empty Optional
. If findPerson(id)
resolves to a person inside of an Optional
, then the lambda person -> person.getMiddleName()
will be called with the person. In that case, two further distinctions happen. If person.getMiddleName()
resolves to an empty Optional
, then the overall result of getPersonMiddleName
is an empty Optional
as well. If person.getMiddleName()
resolves to a string inside of an Optional
, then both calls findPerson(id)
and person.getMiddleName()
have yielded a present value and the overall result of getPersonMiddleName
is the middle name of the person with the given ID inside of an Optional
.
Summary 🔗
With Optionals
, we detected a runtime bug already at compile time. The method Optional.flatMap
allowed us to not have an explicit if-else-case distinction.