There’s an old saying in computer science that null was the “billion dollar mistake.” It’s actually a quote from Tony Hoare, the creator of the null reference.
It’s easy to understand the hate for null
. We’ve all run into null reference exceptions in just about any language we’ve used. But as annoying as they can be, it’s easy to wonder how they can be avoided. After all, it’s inevitable that sometimes you have a variable pending assignment. If not null, how would the absence of a value be represented in a way that prevents a developer from creating exceptions when interacting with that non-value?
Optionals
This post is about the Optional type, which is a common way programming languages protect developers from null references. The idea is, the optional type gives you a box that can be empty (null) or have a value in it. Along with some APIs to safely deal with these possibilities.
This is a concept that exists in many languages. Swift has a particularly elegant implementation, which is integrated into various language-level features. But for this post, we’ll look at Java, which added an Optional type in version 8 of the language. Along the way we’ll run into a few other modern Java features.
The Old Way
Let’s say we have a basic Person
type:
record Person(String name, int age){}
Code language: Java (java)
Records were added to Java 14, and are essentially simplified classes for objects which are mainly carriers of data.
Let’s say we want to declare a variable of type Person
. Normally we’d write:
Person p;
Code language: Java (java)
Then carefully check for null before referencing any properties.
if (p != null) {
System.out.println(p.name);
}
Code language: Java (java)
If you failed to check, you’d be greeted with something like:
Exception in thread "main" java.lang.NullPointerException: Cannot read field "name" because "p" is null
at Main.main(Main.java:13)
Your First Optional
To use the Optional type, make sure to do the proper import:
import java.util.Optional;
Code language: Java (java)
Then declare your variable:
Optional<Person> personMaybe;
Code language: Java (java)
If you don’t have a value to assign, you can indicate that by assigning Optional.empty()
personMaybe = Optional.empty();
Code language: Java (java)
Or, you can assign an actual value with the of
static method.
personMaybe = Optional.of(new Person("Mike", 30));
Code language: Java (java)
If you try to get cute and assign null this way:
personMaybe = Optional.of(null);
Code language: Java (java)
You’ll be greeted by an error immediately.
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:209)
at java.base/java.util.Optional.of(Optional.java:113)
at Main.main(Main.java:12)
Code language: PHP (php)
If you truly have a value that might be null, which you want to safely and correctly assign to an optional, you can use the ofNullable
method:
// where x is of type Person that could be null
personMaybe = Optional.ofNullable(x);
Code language: Java (java)
Using Your Optional
It’s one thing to have an optional type that can hold a value, but how do you use the value? The crudest, most dangerous way to access the value contained in your optional is with the get
method
System.out.println(personMaybe.get().name);
Code language: Java (java)
The get()
method returns the value that’s in the optional if there is one, or if the optional is empty, it will promptly error out.
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.base/java.util.Optional.get(Optional.java:143)
at Main.main(Main.java:21)
There’s an isPresent()
you can call, to check for this.
if (personMaybe.isPresent()) {
System.out.println(personMaybe.get().name);
}
Code language: Java (java)
Here we’re no better off than we were before. These APIs allow you to (carefully) interact with other APIs that are not coded with Optional types. In general, using get
should be avoided where possible.
Let’s see some of the better APIs Optional ships with.
Using Optionals Effectively
If we want to use an optional, rather than carefully calling get
(after verifying there’s a value) we can use the ifPresent()
method.
personMaybe.ifPresent(p -> System.out.println(p.name));
Code language: Java (java)
We pass in a lambda expression that will be invoked with the person
value. If the optional is empty, nothing will be done. If you’d like to also handle the empty use case, we can use ifPresentOrElse
.
personMaybe.ifPresentOrElse(
p -> System.out.println(p.name),
() -> System.out.println("No person")
);
Code language: Java (java)
It’s the same as before, except we now provide a lambda for when the optional is empty.
Getting Values from an Optional
Let’s say we want to get a value from an Optional. Let’s first expand our Person
type just a bit and add a bestFriend
property of type Optional<Person>
.
record Person(String name, int age, Optional<Person> bestFriend){}
Code language: Java (java)
Now let’s say we have a Person
optional:
personMaybe = Optional.of(new Person("Mike", 30, Optional.empty()));
Code language: Java (java)
Then let’s say we want to get that person’s name (and we don’t want to assume there’s a value in there). We want to store this as an Optional<String>
. Obviously we could do something ridiculous like this
Optional<String> personsName = personMaybe.isPresent()
? Optional.of(personMaybe.get().name)
: Optional.empty();
Code language: Java (java)
It should come as no surprise that there’s a more direct API: map
. The map
API takes a lambda expression, from which you return whatever you want from the object. The type system will look at what you return and fit that into an Optional of that type. If there’s no value present in the Optional, the lambda will not be called, and you’ll be safely left with Optional.empty()
Optional<String> personsName = personMaybe.map(p -> p.name);
Code language: Java (java)
Since records automatically create getter methods for all properties, the following would also work:
Optional<String> personsName = personMaybe.map(Person::name);
Code language: Java (java)
The ::
syntax is a method reference, which was added in Java 8.
As of version 10 Java supports inferred typings, so you could also write:
var personsName = personMaybe.map(Person::name);
Code language: Java (java)
The var
keyword takes the meaning from C#, not JavaScript. It does not represent a dynamically typed value. Rather, it’s merely a shortcut where, instead of typing out your type, you can tell the type system to infer the correct type based on what’s on the right hand side of the assignment, and pretend you typed that. Needless to say…
var x;
Code language: Java (java)
… produces a compiler error of:
java: cannot infer type for local variable x
(cannot use 'var' on variable without initializer)
Optionals of Optionals
So far we’ve added the bestFriend
property to our Person record, which is of type Optional<Person>
. Let’s put it to good use.
Optional<Person> personsBestFriend = personMaybe.map(p -> p.bestFriend);
Code language: Java (java)
Rather than use var
, I explicitly typed out the type, so we’d know immediately what was wrong. IntelliJ highlights this line as an error, and when we hover, we’re greeted by this (surprisingly clear) error message.
Required type: Optional<Person>
Provided: Optional<Optional<Person>>
Code language: Java (java)
The value we return from the map
method is placed inside of an optional for us. But, here, the value we return is already an optional, so we’re left with an optional of an optional. If we want to “flatten” this optional of an optional into just an optional, we use flatMap
(just like we use flatMap
in JavaScript when we want to flatten an array of arrays from Array.map
).
Optional<Person> personsBestFriend = personMaybe.flatMap(p -> p.bestFriend);
Code language: Java (java)
We can use this optional now, as we did before.
personsBestFriend.ifPresentOrElse(
s -> System.out.println(s.name),
() -> System.out.println("No person")
);
Code language: Java (java)
Chaining Things Together
Rather than pulling the name off of the best friend, let’s clean the code above up a bit by extracting the best friend’s name directly, and then using that. Let’s also start to use method references more, to remove some of the bloat
Optional<String> bestFriendsName = personMaybe.flatMap(Person::bestFriend).map(Person::name);
Code language: Java (java)
We can use this as before:
bestFriendsName.ifPresentOrElse(System.out::println, () -> System.out.println("Nothing"));
Code language: Java (java)
For one final trick, let’s note that Optionals have an orElse
method. If you have an Optional<T>
, orElse
takes a value (not an optional) of type T. If the optional had a value, that value is returned. If the optional was empty, the value you provided is returned. It’s a good way to convert an optional to a real value, while providing a default value if the optional was empty. Let’s see it in action with the code above, grabbing our person’s best friend’s name (if there is one).
String bestFriendsName = personMaybe
.flatMap(Person::bestFriend)
.map(Person::name)
.orElse("No friend found");
Code language: Java (java)
and now we can use this string, which is guaranteed to not be null.
System.out.println(bestFriendsName);
Code language: Java (java)
Wrapping up
I hope you enjoyed this introduction to Java’s Optional type. It’s a great tool to make your code safer and more clear.