A Comprehensive Guide to Optional in Java
Before diving into Optional, let’s first understand the problem it aims to solve. Imagine we have a UserRepository class that manages a repository of users, which, in this case, is just a simple HashMap. Suppose we need to find a user in the database by their ID and then print the length of their username to the console. At first glance, this seems like a straightforward task.
final Person person = personRepository.findById(1L);
final String firstName = person.getFirstName();
System.out.println(firstName.length());
However, running this code might result in an exception:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.example.optional.Person.getFirstName()" because "person" is null
Why does this happen? The issue lies in the fact that we didn’t account for the possibility that a user with ID 1L might not exist in our HashMap. When a user is not found, our method returns null.
public class PersonRepository {
private final Map<Long, Person> persons;
public PersonRepository(Map<Long, Person> persons) {
this.persons = persons;
}
public Person findById(Long id) {
return persons.get(id);
}
}
When you call a method on a null object, you’ll encounter a NullPointerException. This is a common pitfall, especially for those new to programming. But regardless of whether you’re new to Java or have a decade of experience, encountering a NullPointerException is always a possibility.
Tony Hoare, the inventor of null, famously said, “Inventing the null reference in 1965 was my billion-dollar mistake. I couldn’t resist the temptation to introduce a null reference simply because it was so easy to implement.”
So, what should we do in such cases? One approach is to check all return values for null, like this:
final Person person = personRepository.findById(1L);
if (person != null) {
final String firstName = person.getFirstName();
System.out.println("Your name length: " + firstName.length());
}
If no user exists, the code simply won’t execute. But let’s assume a user with the given ID does exist and we proceed with the code:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "firstName" is null
at com.example.optional.Main.test(Main.java:20)
at com.optional.Main.main(Main.java:13)
Once again, we encounter a NullPointerException, this time because the firstName field is null, and calling the length() method on it throws an exception. No problem—let’s add another check:
final User user = userRepository.findById(1L);
if (user != null) {
final String firstName = user.getFirstName();
if (firstName != null) {
System.out.println("Your name length: " + firstName.length());
}
}
At this point, it’s clear that we’ve gone down the wrong path. These checks clutter our business logic and make the code harder to read. They diminish the overall readability of our program.
This was how we had to manage things before the introduction of Optional in Java. But now, we have a better solution.
Introduction to Optional
You can think of Optional as a kind of box—a wrapper that holds an object. Optional is simply a container: it can contain an object of some type T, or it can be empty.
Let’s rewrite our example using Optional to see how it changes:
public class PersonRepository {
private final Map<Long, Person> persons;
public PersonRepository(Map<Long, Person> persons) {
this.persons = persons;
}
public Optional<Person> findById(Long id) {
return Optional.ofNullable(persons.get(id));
}
}
final Optional<Person> optPerson = personRepository.findById(1L);
if (optPerson.isPresent()) {
final Person person = optPerson.get();
final String firstName = person.getFirstName();
if (firstName != null) {
System.out.println("Your name length: " + firstName.length());
}
}
You might be thinking: This doesn’t look simpler, and it even adds another line! However, a conceptually important shift has occurred. You now know with certainty that the findById() method returns a container where the object might not exist. You no longer need to anticipate a sudden null value—unless the method’s developer is negligent, as nothing prevents them from returning null instead of Optional.
Here, we’ve encountered two key methods: isPresent(), which returns true if an object is present inside, and get(), which retrieves the object.
Now, let’s simplify this code using additional Optional methods to see the benefits they offer. We’ll discuss these methods in more detail below.
final Optional<Person> optPerson = personRepository.findById(1L);
optPerson.map(person -> person.getFirstName())
.ifPresent(
firstName -> System.out.println("Length of your name: " + firstName.length())
);
In this example, we used the map() method, which transforms our Optional into an Optional of another type. Here, we retrieved the user’s first name, converting Optional<Person> to Optional<String>.
This approach can be made even clearer:
final Optional<Person> optPerson = personRepository.findById(1L);
final Optional<String> optFirstName = optPerson.map(person -> person.getFirstName());
optFirstName.ifPresent(
firstName -> System.out.println("Length of your name: " + firstName.length())
);
Finally, we called the ifPresent() method, where we output the result to the console. You can further condense the code:
personRepository.findById(1L)
.map(Person::getFirstName)
.ifPresent(
firstName -> System.out.println("Length of your name: " + firstName.length())
);
Optional Methods
Now that we understand why we need Optional and have seen an example of its use, let’s explore the rest of the methods this class offers. Optional comes with a variety of methods that, when used correctly, can help you write simpler and more understandable code.
Creating an Optional
This class doesn’t have constructors, but it provides three static methods to create instances.
Optional.ofNullable(T t)
Optional.ofNullable accepts any type and creates a container. If you pass null to this method, it will create an empty container. Use this method when there’s a chance the object might be null.
public class PersonRepository {
private final Map<Long, Person> persons;
public PersonRepository(Map<Long, Person> persons) {
this.persons = persons;
}
public Optional<Person> findById(Long id) {
return Optional.ofNullable(persons.get(id));
}
}
In this example, we use ofNullable() because the Map#get() method might return null.
Optional.of(T t)
This method is similar to Optional.ofNullable, but if you pass a null value to the parameter, it throws a NullPointerException. Use this method when you are certain the value should not be null.
public class PersonRepository {
private final Map<Long, Person> persons;
public PersonRepository(Map<Long, Person> persons) {
this.persons = persons;
}
public Optional<Person> findByLogin(String login) {
for (Person person : persons.values()) {
if (person.getLogin().equals(login)) {
return Optional.of(person);
}
}
return Optional.empty();
}
}
In this new method, we find a user by login. We iterate through the Map values and compare user logins with the one provided. Once a match is found, we call the Optional.of() method, knowing that such an object exists.
Optional.empty()
But what if the user with the given login isn’t found? The method should still return an Optional. You could call Optional.ofNullable(null) and return it, but it’s better to use the Optional.empty() method, which returns an empty Optional.
Note the use of Optional.empty() in the last example.
Retrieving Content
We’ve explored methods to create an Optional object. Now, let’s look at methods for retrieving values from the container.
isPresent()
Before retrieving an object, it’s wise to check if it’s actually there. The isPresent() method returns true if an object is present in the container, and false otherwise.
Optional<Person> optPerson = personRepository.findById(1L);
if(optPerson.isPresent()) {
// If the user is found, execute this block of code
}
Essentially, this is a standard check, akin to writing if (value != null). If you examine the implementation of isPresent(), you’ll see:
public boolean isPresent() {
return value != null;
}
isEmpty()
The isEmpty() method is the opposite of isPresent(). It returns true if there is no object inside, and false otherwise. The implementation looks like this:
public boolean isEmpty() {
return value == null;
}
get()
After confirming the presence of an object with the previous methods, you can safely retrieve it using the get() method.
Optional<Person> optPerson = personRepository.findById(1L);
if(optPerson.isPresent()) {
final Person person = optPerson.get();
// The rest of your code
}
Of course, you can call the get() method without checking, but if the object isn’t there, you’ll receive a NoSuchElementException.
ifPresent()
Besides isPresent(), there’s the ifPresent() method, which takes the Consumer functional interface as an argument. This allows you to perform some logic on the object if it exists.
Let’s use this method to print the user’s first and last name to the console:
personRepository.findById(id).ifPresent(
person -> System.out.println(person.getFirstName() + " " +
person.getLastName())
);
ifPresentOrElse()
The ifPresent() method does nothing if there’s no object, but if you need to execute some code regardless, use the ifPresentOrElse() method, which also accepts the Runnable functional interface as a parameter.
Here’s an example where we print “John Doe” to the console if the user isn’t found:
personRepository.findById(id).ifPresentOrElse(
person -> System.out.println(person.getFirstName() + " " +
person.getLastName()),
() -> System.out.println("John Doe")
);
orElse()
This method does exactly what its name suggests: it returns the value in the container or a default value that you specify. For instance, we can return a user by ID, and if that user doesn’t exist, return an anonymous user:
public Person getPersonOrAnon(Long id) {
return personRepository.findById(id)
? .orElse(new Person(-1L, "anon", "anon", "anon"));
}
Use this method when you want to return a default value if the container is empty.
Some developers use the odd construction objectOptional.orElse(null), which negates the purpose of Optional. Avoid doing this, and discourage others from doing it as well.
领英推荐
orElseGet()
This method is similar to the previous one, but instead of returning a value, it executes the Supplier functional interface. Let’s take the above example and also print a message to the console indicating the user wasn’t found:
public Person getPersonOrAnonWithLog(Long id) {
return personRepository.findById(id)
.orElseGet(() -> {
System.out.println("User was not found, returning anonymous");
return new Person(-1L, "anon", "anon", "anon");
});
}
Use this method when you don’t just need to return a default value but also need to perform some more complex logic.
orElseThrow()
This method returns the object if it exists; otherwise, it throws the standard exception NoSuchElementException("No value present").
orElseThrow(Supplier<? extends X> exceptionSupplier)
This variation allows you to throw any exception instead of the standard NoSuchElementException if the object is not present.
public Person getPersonOrThrow(Long id) {
return personRepository.findById(id)
.orElseThrow(() -> new NotFoundException("User not found"));
}
Elegant orElseThrow
How about this elegant usage of orElseThrow()?
public Person getPersonOrElegantThrow(Long id) {
return personRepository.findById(id)
.orElseThrow(notFoundException("User {0} not found", id));
}
In my opinion, this is the most aesthetic usage. To achieve this, you need to add a method in the exception class that returns a Supplier. Here’s an example of NotFoundException, where we use two notFoundException methods: one accepts a simple string, and the other uses MessageFormat.format() to format a message with parameters.
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
public NotFoundException(String message, Object... args) {
super(MessageFormat.format(message, args));
}
public static Supplier<NotFoundException> notFoundException(String message,
Object... args) {
return () -> new NotFoundException(message, args);
}
public static Supplier<NotFoundException> notFoundException(String message) {
return () -> new NotFoundException(message);
}
}
Conversion
Optional also includes several methods you might recognize from streams: map, filter, and flatMap. These methods share the same names and perform similar functions.
filter()
If the container holds a value and it satisfies the condition specified by a Predicate functional interface, the filter method returns a new Optional object containing that value; otherwise, it returns an empty Optional.
For example, here’s a method that returns only adult users by their ID:
public Optional<Person> getAdultById(Long id) {
return personRepository.findById(id)
.filter(person -> person.getAge() > 18);
}
Use this method when you need a container whose object meets a specific condition.
map()
If a value exists inside the container, the map method applies a given function to it, places the result in a new Optional, and returns it. If no value exists, an empty container is returned. The Function interface is used for this conversion.
Here’s a method that returns an Optional<String> containing the first and last names of a user, based on their ID:
public Optional<String> getFirstAndLastNames(Long id) {
return personRepository.findById(id)
.map(person -> person.getFirstName() + " " + person.getLastName());
}
Use this method when you need to transform an object inside a container into another object.
flatMap()
As mentioned, the map method wraps the result of the Function lambda. However, if this lambda returns an already wrapped result, you end up with Optional<Optional<T>>. Working with such a double-packed object can be cumbersome.
For instance, if you request some data about a user from another service, and this data might also be unavailable, a second container appears. Here’s how it might look:
Optional<Optional<String>> optUserPhoneNumber = personRepository.findById(1L)
.map(person -> {
Optional<String> optPhoneNumber =
phoneNumberRepository.findByPersonId(person.getId());
return optPhoneNumber;
});
In such cases, use flatMap() to eliminate the extra container:
Optional<String> optUserPhoneNumber = personRepository.findById(1L)
.flatMap(person -> {
Optional<String> optPhoneNumber =
phoneNumberRepository.findByLogin(user.getLogin());
return optPhoneNumber;
});
stream()
Introduced in Java 9, the stream() method in Optional enables convenient work with streams from a collection of Optional elements.
Let’s say you’ve queried multiple users by different IDs, and the response returned is Optional<Person>. You’ve added these results into a collection, and now you need to process them. You can create a stream from this collection and use the stream’s flatMap() method along with the Optional stream() method to obtain a stream of existing users. All empty containers will be discarded.
final List<Optional<Person>> optPeople = new ArrayList<>();
final List<Person> people = optPeople.stream()
.flatMap(optItem -> optItem.stream())
.toList();
or()
Starting with Java 11, the or() method was introduced. It allows you to replace an empty Optional by providing a new object, something that was not possible before. It’s important to note that this method doesn’t modify the Optional object but rather creates and returns a new one.
For example, you can query a user by ID, and if they are not found, you return an Optional containing an anonymous user:
public Optional<Person> getPersonOrAnonOptional(Long id) {
return personRepository.findById(id)
.or(() -> Optional.of(new Person(-1L, "anon", "anon", "anon", 0L)));
}
Combining Methods
All of these methods return an Optional result, allowing you to chain them together, just like with streams. Here’s a hypothetical example that illustrates method chaining:
final LocalDateTime now = LocalDateTime.now();
final DayOfWeek dayWeek = Optional.of(now)
.map(LocalDateTime::getDayOfWeek)
.filter(dayOfWeek -> DayOfWeek.SUNDAY.equals(dayOfWeek))
.orElse(DayOfWeek.MONDAY);
Other Nuances
Below are additional tips and practical insights that will help you master this tool even better.
When to Use Optional
If you refer to the Javadoc for Optional, you’ll find a direct answer to this question. Optional is primarily intended for use as a method return type when there is a clear need to represent “no result,” and where using null might cause errors.
How NOT to Use Optional
Now, let’s look at some common mistakes when using Optional.
As a Method Parameter
You should avoid using Optional as a method parameter. If the user of a method with an Optional parameter is unaware of this, they might pass null to the method instead of Optional.empty(). Handling null will result in a NullPointerException.
As a Class Property
Additionally, it’s advisable not to use Optional to declare class properties.
Firstly, you might encounter issues with popular frameworks like Spring Data or Hibernate. Hibernate cannot map values from the database to Optional directly without custom converters.
Although some frameworks, such as Jackson, integrate Optional seamlessly into their ecosystem, you might consider using Optional as a class property when you have no control over the creation of an object in this class—for example, when using a Webhook.
Secondly, remember that Optional is an object that is typically only needed for a short period, after which it can be disposed of by the garbage collector. However, it takes time to create an object, and if we store Optional as a field, it can persist until the program stops. While this is unlikely to cause issues in small applications, it’s something to keep in mind.
Thirdly, using such fields can be inconvenient. Optional does not implement the Serializable interface. Simply put, any object containing at least one Optional field cannot be serialized. Although with the rise of microservices, platform serialization is not as critical as before.
A possible solution in this situation is to use Optional for class getters. However, this approach has one drawback: it cannot be fully integrated with Lombok. Optional getters are not supported by the library, and based on discussions on GitHub, they likely won’t be in the future.
As a Collection Wrapper
Don’t wrap collections in Optional. Any collection is already a container. To return an empty collection instead of null, you can use methods like Collections.emptyList(), Collections.emptySet(), and others.
Optional Must Not Be null
Assigning null instead of an Optional object defeats the purpose of using it. None of the users of your method will check if Optional is equivalent to null. Instead of assigning null, use Optional.empty().
Primitives and Optional
To work with primitive wrappers, Java provides java.util.OptionalDouble, java.util.OptionalInt, and java.util.OptionalLong, which allow you to avoid unnecessary autoboxing and unboxing. However, these are rarely used in practice.
These classes are similar to Optional but lack conversion methods. They only offer the following: get, orElse, orElseGet, orElseThrow, ifPresent, and isPresent.
Let Me Summarize
The Optional class doesn’t completely eliminate the NullPointerException problem, but when used correctly, it can reduce the number of errors and make the code more readable and concise. Using Optional is not always appropriate, but it works exceptionally well for method return values.
Key Points to Remember