Back to main page Traveling Coderman

Avoid Runtime Errors with Optionals

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 the Optional 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 findPersonalways 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.