“The introduction of null references was my billion dollar mistake” – Tony Hoare
Optional is a (typed) container object. It may contain a single object, or it may be empty. It allows you to avoid null pointer exceptions. In this article I’m going to work through a number of examples of how to use Optional. I’ll include code snippets, but all the source code is available on my github at:
https://github.com/hedleyproctor/java8-examples
Let’s get started. My examples use the domain of the insurance industry, since that’s the industry I work in. Suppose we have a service that allows you to find an insurance claim based on its id. Prior to Java 8, the method signature of this would be as follows:
public Claim find(Long id)
What’s wrong with this? Well, you don’t know if it could ever return null. Will it? Or will it return a default value? If you want to use any fields of the returned object, you are forced to insert null checks, like this:
Claim claim = claimService.find(id);
if (claim != null) {
productType = claim.getProductType();
}
If you forget the null check, you may get the dreaded NullPointerException. The purpose of Optional is to allow your method signature to tell the caller that the method may not return an object, and make it easier to avoid null pointers. With an Optional, the method call looks like this:
Optional<Claim> optionalClaim = claimService.findById(15l);
The “functional” way to interact with an Optional is not to directly unbox it, but rather to invoke one of the functional methods. e.g.
optionalClaim.ifPresent(claim -> System.out.println("Found claim. Id: " + claim.getId()));
Now, the clever thing is that if we want to use any fields of the returned object, we no longer need to write an explicit null check. Instead, the Optional class has a method called “map”. The contract for map says that you pass it two things, an Optional
, and a lambda or method reference that takes a parameter of type T, and returns something of type U. It then does the following:
- If the Optional is empty, just returns an empty Optional.
- If the Optional has an object inside, invokes the function you have passed it on that object, and wraps the return result in an Optional.
This means that if we want to extract the productType of the claim, as before, we can now write the following:
Optional<Claim.PRODUCT_TYPE> optionalProductType =
claimService.findById(15l)
.map(Claim::getProductType);
Much better! Let’s look at some more variations. Firstly, if you want to provide a default value, you can chain another call to orElse on the end:
Claim.PRODUCT_TYPE myProductType =
claimService.findById(15l)
.map(Claim::getProductType)
.orElse(Claim.PRODUCT_TYPE.MOTOR);
You can even call a supplier function to return the default value if needed:
Claim.PRODUCT_TYPE myProductType2 =
claimService.findById(15l)
.map(Claim::getProductType)
.orElseGet(claimService::getDefaultProductType);
Now, suppose you want to call map with a function, but that function already wraps its response in an Optional. Imagine we want pass our Optional Claim to the following:
public Optional<AuditLog> findAuditLog(Claim claim)
What’s the problem here? Well, remember what the contract of map is. If you give it an Optional with something inside, it passes that to the method you’ve given it, AND THEN WRAPS THE RETURNED OBJECT IN AN OPTIONAL. Yikes! The findAuditLog method returns an Optional (that may or may not have an AuditLog object) but then map would wrap this in a second Optional! We don’t want this, so what is the solution? The answer is that Optional has another method called flatMap. flatMap does not wrap the returned value in an Optional, so we can now write the following:
Optional<AuditLog> auditLogOptional =
claimService.findById(15l)
.flatMap(claimService::findAuditLog);
Optional also has a filter method. Again, it is null safe, so you can safely invoke it on an Optional that might be empty, like this:
Optional<Claim> optionalMotorClaim =
claimService.findById(15l)
.filter(claim -> Claim.PRODUCT_TYPE.MOTOR.equals(claim.getProductType()));
If you really do need to get the value out of an Optional, you can do so, as follows:
if (optionalClaim.isPresent()) {
Claim myClaim = optionalClaim.get();
// do stuff with claim
}
Note that you should ALWAYS call isPresent() prior to calling get(), as get() will throw an exception if you invoke it on an empty Optional. Most of the time, calling ifPresent and passing a lambda will be sufficient for processing your Optional, but extracting the value will be necessary if you need to do stuff that isn’t allowed inside a lambda, such as throwing an exception.
Finally, a side note about one limitation of Optional and Stream in Java 8. At the moment it is a bit convoluted to map a Stream> to extract the values. You have to do the following:
Stream<Claim> claimsLoadedById = claimIdSet.stream()
.map(claimService::findById)
.filter(Optional::isPresent)
.map(Optional::get);
In Java 9, this has been simplified to:
Stream<Claim> claimsLoadedById = claimIdSet.stream()
.map(claimService::findById)
.flatMap(Optional::stream)
Summary
In this article I’ve introduced Optional and given a number of examples of how to use it. To make effective use of Optional you should:
- Use it as the return type for methods that can validly not return an object
- Chain calls to map, flatMap and filter on a returned Optional to avoid nested null pointer checks
This article is part of a series on Java 8. You might be interested in the other articles:
Java 8 Streams Tutorial
Yet another Java 8 custom collector example
I also recommend the book Java 8 In Action.