Introduction
When writing any kind of code in Java, developers tend to work with objects more often than with primitive values (int
, boolean
, etc). This is because objects are at the very essence of object-oriented programming: they allow a programmer to write abstract code in a clean and structured manner.
Furthermore, every object in Java can either contain a value or not. If it does, its value is stored on the heap and the variable which we are using has a reference to that object. If the object contains no value, this defaults to null
- a special placeholder denoting the absence of a value.
The fact that each object can become null
, combined with the natural tendency to use objects instead of primitives, means that some arbitrary piece of code might (and often-times will) result in an unexpected NullPointerException
.
Before the Optional
class was introduced in Java 8, these kind of NullPointerException
errors were much more common in everyday life of a Java programmer.
In the following sections, we will dive deeper into explaining Optional
and seeing how it can be used to overcome some of the common problems concerning null values.
The Optional Class
An Optional is essentially a container. It is designed either to store a value or to be "empty" if the value is non-existent - a replacement for the null
value. As we will see in some later examples, this replacement is crucial as it allows implicit null-checking for every object represented as an Optional
.
This means that explicit null-checking is no longer needed from a programmer's point of view - it becomes enforced by the language itself.
Creating Optionals
Let's take a look at how easy it is to create instances of Optional
and wrap objects that we already have in our applications.
We'll be using our custom class for this, the Spaceship
class:
public class Spaceship {
private Engine engine;
private String pilot;
// Constructor, Getters and Setters
}
And our Engine
looks like:
public class Engine {
private VelocityMonitor monitor;
// Constructor, Getters and Setters
}
And furthermore, we've got the VelocityMonitor
class:
public class VelocityMonitor {
private int speed;
// Constructor, Getters and Setters
}
These classes are arbitrary and only serve to make a point, there's no real implementation behind them.
of()
The first approach to creating Optional
s is using the .of()
method, passing a reference to a non-null object:
Spaceship falcon = new Spaceship();
Optional<Spaceship> optionalFalcon = Optional.of(falcon);
If the falcon
was null
, the method .of()
would throw a NullPointerException
.
Without Optional
, trying to access any of the fields or methods of falcon
(assuming it's null
), without performing a null-check would result in a crash of the program.
With Optional
, the .of()
method notices the null
value and throws the NullPointerException
immediately - potentially also crashing the program.
If the program crashes in both approaches, why even bother using
Optional
?
The program wouldn't crash somewhere deeper in the code (when accessing falcon
) but at the very first use (initialization) of a null
object, minimizing potential damage.
ofNullable()
If falcon
is allowed to be a null
, instead of the .of()
method, we'd use the .ofNullable()
method. They perform the same if the value is non-null
. The difference is obvious when the reference points to null
in which case - the .ofNullable()
method is perfectly contempt with this piece of code:
Spaceship falcon = null;
Optional<Spaceship> optionalFalcon = Optional.ofNullable(falcon);
empty()
And finally, instead of wrapping an existing reference variable (null
or non-null
), we can create a null
value in the context of an Optional
. It's kind of like an empty container which returns an empty instance of Optional
:
Optional<Spaceship> emptyFalcon = Optional.empty();
Checking for Values
After creating Optional
s and packing information in them, it's only natural that we'd want to access them.
Before accessing though, we should check if there are any values, or if the Optional
s are empty.
isPresent()
Since catching exceptions is a demanding operation, it would be better to use one of the API methods to check if the value exists before trying to access it - and alter the flow if it doesn't.
If it does, then .get()
method can be used to access the value. Though, more on that method in latter sections.
To check if the value is present inside an Optional
, we use the .isPresent()
method. This is essentially a replacement for the null
-check of the old days:
// Without Optional
Spaceship falcon = hangar.getFalcon();
if (falcon != null) {
System.out.println(falcon.get());
} else {
System.out.printn("The Millennium Falcon is out and about!");
}
// With Optional
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
if (optionalFalcon.isPresent()) {
System.out.println(falcon.get());
} else {
System.out.println("The Millennium Falcon is out and about!");
}
Since the falcon
can also not be in the hangar, we can also expect a null
value, thus .ofNullable()
is used.
ifPresent()
To make things even easier, Optional
also contains a conditional method which bypasses the presence check entirely:
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
optionalFalcon.ifPresent(System.out::println);
If a value is present, the contents are printed through a Method Reference. If there's no value in the container, nothing happens. You still might want to use the previous approach if you'd like to define an else {}
statement, though.
This reflects what we mentioned earlier when we said that null
-checks with Optional
are implicit and enforced by the type-system.
isEmpty()
Another way to check for a value is to use .isEmpty()
. Essentially, calling Optional.isEmpty()
is the same as calling !Optional.isPresent()
. There's no particular difference that exists:
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
if (optionalFalcon.isEmpty()) {
System.out.println("Please check if the Millennium Falcon has returned in 5 minutes.");
} else {
optionalFalcon.doSomething();
}
Nested Null-Checks
Our Spaceship
class, as defined earlier, has an attribute Engine
, which has an attribute VelocityMonitor
.
Suppose now that we want to access the velocity monitor object and obtain the current velocity of the spaceship, taking into consideration that all these values could potentially be null
.
Obtaining the velocity might look something like this:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
if (falcon != null) {
Engine engine = falcon.getEngine();
if (engine != null) {
VelocityMonitor monitor = engine.getVelocityMonitor();
if (monitor != null) {
Velocity velocity = monitor.getVelocity();
System.out.println(velocity);
}
}
}
The above example shows how tedious it is to perform such checks, not to mention the amount of boilerplate code needed to make the checks possible in the first place.
An alternative solution using Optional
would be:
Velocity velocity = falcon
.flatMap(Spaceship::getEngine)
.flatMap(Engine::getVelocityMonitor)
.map(VelocityMonitor::getVelocity);
Note: Not sure what's going on above? Check out the explanation below for the details.
Using this kind of approach, no explicit checks are needed. If any of the objects contain an empty Optional
, the end result will also be an empty Optional
.
To make things work like this, we need to modify our existing definitions of the Spaceship
and Engine
classes:
public class Spaceship {
private Optional<Engine> engine;
private String pilot;
// Constructor, Getters and Setters
}
public class Engine {
private Optional<VelocityMonitor> monitor;
// Constructor, Getters and Setters
}
What we have changed are the attribute definitions: they are now wrapped inside Optional
objects to make this kind of alternative solution possible.
This might seem a bit tedious at first but if planned from the beginning, it takes almost the same amount of effort typing it.
Furthermore, having an Optional
attribute instead of a regular object reflects the fact that the attribute might or might not exist. Notice how this is quite helpful since we don't have semantic meanings of this kind with regular attribute definitions.
Example Explanation
In this section, we'll take a bit of time to explain the previous example with flatMaps
and maps
. If you understand it without further explanation, please feel free to skip this section.
The first method call is performed on falcon
which is of type Optional<Spaceship>
. Calling the getEngine
method returns an object of type Optional<Engine>
. Combining these two types, the type of the returned object becomes Optional<Optional<Engine>>
.
Since we would like to view this object as an Engine
container and perform further calls on it, we need some kind of mechanism to "peel off" the outer Optional
layer.
Such a mechanism exists and it is called flatMap
. This API method combines the map
and the flat
operations by first applying a function to each of the elements and then flattening the result into a one-level stream.
The map
method, on the other hand, only applies a function without flattening the stream. In our case, the use of map
and flatMap
would give us Optional<Optional<Engine>>
and Optional<Engine>
respectively.
Calling flatMap
on an object of type Optional
would therefore yield with a one-level Optional
, allowing us to use multiple similar method calls in a succession.
This finally leaves us withOptional<Engine>
, which we wanted in the first place.
Alternative results
.orElse()
The previous example can be further expanded by using the orElse(T other)
method. The method will return the Optional
object upon which it is called only if there is a value contained within it.
If the Optional
is empty, the method returns the other
value. This is essentially an Optional
version of the ternary operator:
// Ternary Operator
Spaceship falcon = maybeFalcon != null ? maybeFalcon : new Spaceship("Millennium Falcon");
// Optional and orElse()
Spaceship falcon = maybeFalcon.orElse(new Spaceship("Millennium Falcon"));
As with the ifPresent()
method, this kind of approach takes advantage of the lambda expressions to make code more readable and less error-prone.
.orElseGet()
Instead of providing the other
value directly as an argument, we can use a
Supplier instead. The difference between .orElse()
and .orElseGet()
, while maybe not evident at first glance, exists:
// orElse()
Spaceship falcon = maybeFalcon.orElse(new Spaceship("Millennium Falcon"));
// orElseGet()
Spaceship falcon = maybeFalcon.orElseGet(() -> new Spaceship("Millennium Falcon"));
If maybeFalcon
doesn't contain a value, both methods will return a new Spaceship
. In this case, their behavior is the same. The difference becomes clear if maybeFalcon
does contain a value.
In the first case, the new Spaceship
object will not be returned but it will be created. This will happen regardless of whether or not the value exists. In the second case, the new Spaceship
will be created only if maybeFalcon
doesn't contain a value.
It's similar to how do-while
does the task regardless of the while
loop, at least once.
This might seem like a negligible difference but it becomes pretty important if creating spaceships is a demanding operation. In the first case, we're always creating a new object - even if it will never be used.
.orElseGet()
should be preferred instead of .orElse()
in such cases.
.orElseThrow()
Instead of returning an alternative value (as we've seen in the previous two sections), we can throw an exception. This is accomplished with the .orElseThrow()
method which instead of an alternative value accepts a supplier which returns the exception in case it needs to be thrown.
This can be useful in cases where the end result is of high importance and must not be empty. Throwing an exception in this case might be the safest option:
// Throwing an exception
Spaceship falcon = maybeFalcon.orElseThrow(NoFuelException::new);
Getting Values from Optional
.get()
After seeing many different ways of checking and accessing the value inside Optional
, let's now take a look at one final way of obtaining the value which also uses some of the previously shown methods.
The simplest way to access a value inside an Optional
is with .get()
. This method returns the value present, or throws a NoSuchElementException
if the value is absent:
Optional<Spaceship> optionalFalcon = Optional.ofNullable(hangar.getFalcon());
if (falcon.isPresent()) {
Spaceship falcon = optionalFalcon.get()
// Fly the falcon
}
As expected, the .get()
method returns a non-null
instance of the Spaceship
class and assigns it to the falcon
object.
Conclusion
Optional
was introduced to Java as a way to fix the issues with null
references. Before Optional
, every object was allowed to either contain a value or not (i.e. being null
).
The introduction of Optional
essentially enforces null
-checking by the type-system making it unnecessary to perform such checks manually.
This was a big step both in improving the language and its usability by adding an additional layer of type-checking. Using this system instead of the old-fashioned null
-checking allows writing clear and concise code without the need to add boilerplate and perform tiring checks by hand.