The Java Optional class: 11 more recipes for preventing null pointer exceptions

How to avoid Optional class antipatterns and design smells—all while streamlining application development

July 20, 2020

Download a PDF of this article

Are you tired of null pointer exceptions? The recent Java Magazine article “12 recipes for using the Optional class as it’s meant to be used” talked about the Optional class, which is a container type for a value that may be absent. That previous article looked at when to use Optional and when not to use Optional. Further, it introduced several corner cases and temptations, which can be considered traps that could downgrade the quality of your code or even cause unexpected behaviors. Those 12 recipes covered three categories:

  • Why am I getting null even when I use Optional?
  • What should I return or set when no value is present?
  • How do I consume Optional values effectively?

This article provides 11 more recipes to cover two additional categories:

  • How do I avoid Optional antipatterns?
  • I like Optional; what can I do more professionally?

The previous article scratched the Optional class’s surface—best practices to efficiently prevent null issues, make the best use of its methods, and prevent some performance problems that might arise when using Optional. However, for some developers, using Optional in the right way isn’t as straightforward as it may seem.

Like any other programming language feature, Optional can be used correctly or abused. So, I’ll delve deeply into some Optional antipatterns and design smells I’ve noticed from developers’ code and even my own code. Then I’ll go through more-advanced recipes that deal with the Stream API, transformations, and many cases to cover the use of all the remaining Optional class methods. [The term “design smell” refers to “structures in the design that indicate violation of fundamental design principles and negatively impact design quality,” according to Girish Suryanarayana, Ganesh Samarthyam, and Tushar Sharma in “Refactoring for Software Design Smells: Managing Technical Debt.” —Ed.]

First understand the purpose of the exercise. Optional is an effort to minimize the number of null pointer exceptions in Java by introducing the possibility of creating more flexible APIs that allow a method’s return type to indicate “no result” without using the null reference to indicate the missing value.

In my opinion, if the Optional class had existed from the earliest days of Java, most of the libraries and applications would probably handle the missing return values effectively, which would reduce the possibilities of null pointer exceptions and the total number of bugs in general. Sadly, that’s not the way Java started. The good news is Java does have Optional now.

Let’s jump in.

How do I avoid Optional antipatterns?

This question generally categorizes the confusion about the primary purpose of the Optional class functionality. Such confusion leads to many antipatterns that make the code verbose and not as efficient as it should be. This confusion also exposes a lousy API design and introduces memory problems in some cases.

Recipe 13: Optional should not be used as a field type. I found that developers use Optional as a field type in 45% of the code I have reviewed. For example:


public class Account {

    enum TYPE {SAVINGS, CREDIT, DEBIT;}

    private String name;
    private Optional<TYPE> type = Optional.of(TYPE.SAVINGS);
    private OptionalDouble balance = OptionalDouble.empty();

    public Account(String name, OptionalDouble balance) {
        this.name = Objects.requireNonNull(name, () -> "Account Name cannot be null.");
        this.balance = balance;
    }

    public Account(String name, OptionalDouble balance, Optional<TYPE> type) {
        this(name, balance);
        this.type = type;
    }

    public void setType(Optional<TYPE> type) {
        this.type = type;
    }

    public Optional<TYPE> getType() {
        return type;
    }    
    ...
}

On the one hand, this kind of code is an antipattern to avoid the null reference. On the other hand, it complicates the use of the Account class, because it requires the caller to use Optional to satisfy the Account object methods, constructors, and setters arguments, which would lead to unnecessarily complicated and verbose code:


Account acc = new Account("Euro Account", OptionalDouble.of(1453.70), Optional.of(Account.TYPE.CREDIT));

acc.getBalance().ifPresent(System.out::println);
acc.getType().ifPresent(System.out::println);

Whenever you use Optional, you also increase the memory usage footprint, because Optional adds another object layer that wraps the original value. The Optional class was originally intended to be used mainly as a method return type, not as a data type, so keep everything as simple as you can:


public class Account {
    [access_modifier] [static] [final] String name;
    [access_modifier] [static] [final] Type type = TYPE.SAVINGS;
    [access_modifier] [static] [final] double balance = 0.0;
    ...
}

API note: The Optional class doesn’t implement the Serializable interface; therefore, it is definitively not meant to be a JavaBean property.

Recipe 14: Optional should not be used in the constructor argument. Because Optional is not designed to be used as a field type, conclude a rule here:

Rule: Do not use Optional as the field type or in the constructor, method, or setters arguments.

Another antipattern of using Optional is that developers tend to force all constructor arguments to accept Optional when they wrap the original value. Developers do this to indicate that some values could be Optional and might be missing, as in the following case for balance:


public class Account {
    ...
    private String name;
    private OptionalDouble balance = OptionalDouble.empty();

    public Account(String name, OptionalDouble balance) {
        this.name = Objects.requireNonNull(name, () -> "Account Name cannot be null.");
        this.balance = balance;
    }
    
    public OptionalDouble getBalance() {
        return balance;
    }    
    ...
}

What’s the problem? Besides the code reading like boilerplate, it will fail when you try to create an Account object like this:


Account acc = new Account("Euro Account", null);

acc.getBalance().ifPresent(System.out::println);
acc.getType().ifPresent(System.out::println);
System.out.println(acc.getName());

Notice the NullPointerException that the developer tried to hide by using Optional. Fix this in the balance getter method (see the changes to the constructor):


private Double balance = 0.0;

public Account(String name, Double balance) {
    this.name = Objects.requireNonNull(name, () -> "Account Name cannot be null.");
    this.balance = balance;
}

public Optional<Double> getBalance() {
    return Optional.ofNullable(balance);
}

API note: Please don’t take it as a rule of thumb to insist that all your getter methods return Optional. Doing so will be considered overuse of the Optional class. Use this technique only when it’s needed.

Recipe 15: Optional should not be used in the method’s arguments. There is a big debate about using Optional as a method argument. I am not convinced that anyone should use Optional as a method argument, and I consider doing so a common antipattern source of design smell. By the way, some code inspectors such as SonarQube officially consider using Optional for parameters to be a design smell.

Despite the rule in recipe 14, let’s see whether it is relevant to use Optional as a method argument.

Suppose a developer is writing a method to search a list of employees by matching their names. The method might also optionally filter by department. The developer opens the IDE and starts typing happily:


public class Employee {

    int id;
    String name;
    String department;
    ...
}

public class EmployeeService {
    
    public List<Employee> searchEmployee(
            List<Employee> 
            employees, String name, 
            Optional<String> department){
        
        // Null checks for employees and name
        return employees.stream()
                .filter(employee -> employee.getName().matches(name))
                .filter(employee -> employee.getDepartment().matches(department.orElse(".*")))
                .collect(toList());
    }
}

The code looks promising as an elegant solution for handling the department presence, and so the developer releases it to the code repository.

Another developer pulls the code and starts to use it, thinking, “Hmm, I need to search for all employees regardless of their department.”


service.searchEmployee(employees, ".*", null)
                .stream()
                .forEach(System.out::println);

Kaboom! The code throws a NullPointerException, which is clearly not the intended handling of the department parameter in the initial method design. Moreover, that exception makes it complicated to use the method correctly by forcing the method caller to depend on Optional, because the method is too lazy to check for nullability.


EmployeeService service = new EmployeeService();
        
var employees = List.of(
    new Employee(1, "Mohamed Taman", "Business Development"),
    new Employee(1, "Malik Taman", "HR"));

service.searchEmployee(employees, "Mo.*", Optional.empty())
        .stream()
        .forEach(System.out::println);

service.searchEmployee(employees, ".*", Optional.<String>of("HR.*"))
        .stream()
        .forEach(System.out::println);

To fix this design issue and make it clearer, introduce two overloaded methods that do two different things even when they share the same implementation:



private List<Employee> searchEmployee0(
            List<Employee> employees, String name,
            String department) {

        Objects.requireNonNull(employees, "Employees can't be null.");
        Objects.requireNonNull(name, "Name can't be null.");
        final var departmentFilter = Objects.requireNonNullElse(department, ".*");

        return employees.stream()
                .filter(employee -> employee.getName().matches(name))
                .filter(employee -> employee.getDepartment().matches(departmentFilter))
                .collect(toList());
}

public List<Employee> searchEmployee(
        List<Employee> employees, String name) {
    return searchEmployee0(employees, name, ".*");
}

public List<Employee> searchEmployee(
        List<Employee> employees, String name,
        String department) {
    return searchEmployee0(employees, name, department);
}

Now it is safe to use the method and dodge any unhappy runtime surprises:


service.searchEmployee(employees, "Mo.*")
        .stream()
        .forEach(System.out::println);

service.searchEmployee(employees, ".*", "HR.*")
        .stream()
        .forEach(System.out::println);

service.searchEmployee(employees, ".*", null)
        .stream()
        .forEach(System.out::println);

Reference: Grzegorz Ziemoński wrote an excellent post on this topic, “Optional Method Parameters,” on DZone.

Recipe 16: Do not use Optional as an argument to setters. In some code, developers use Optional as a property in Java Persistence API (JPA) entities in case the property is not present. I don’t like this design, because it forces the setter method to accept Optional as an argument wrapping the original value. That approach complicates the code and creates extra dependency for the caller of this setter method:


@Entity
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    int id;
    ...
    @Column(name = "employee_address")
    private Optional<String> address; // optional field, thus may be null

    public void setAddress(Optional<String> address) {
        this.address = address;
    }

    public Optional<String> getAddress() {
        return address;
    }
    ...
}

Therefore, I consider this an antipattern and a design smell. Don’t forget that Optional is not Serializable. Here’s how to avoid the problems:


@Column(name = "employee_address")
private String address; // optional field, thus may be null

public Employee() {
}

public void setAddress(String address) {
    this.address = address;
}

public Optional<String> getAddress() {
    return Optional.ofNullable(address);
}

Recipe 17: Do not assert the emptiness of arrays or collections by using Optional. Yes, I mentioned that Optional mainly should be used as a method return type. However, I’ve seen code that tries to avoid returning empty or null values by wrapping all the return values within an Optional instance. Such an implementation is considered one of the common antipatterns and a design smell. Consider the following example:


public class EmployeeService {

    public Optional<List<Employee>> getEmployeeByDepartment(String department) {
        // May return null or empty list
        List<Employee> employees = searchEmployee(".*", department);
        // So, wrap it with optional
        return Optional.ofNullable(employees);
    }
}

This code adds a layer of complication. In this case, simply rely on returning an empty collection or array by using the Collections class emptySet(), emptyList(), or emptyMap() methods as in the following:


public List<Employee> getEmployeeByDepartment(String department) {
    var employees = searchEmployee(".*", department);
    return employees != null? employees : Collections.emptyList();
}

API note: The Collections class methods emptySet(), emptyList(), and emptyMap() have been around since Java 1.5.

I like Optional; what can I do more professionally?

The previous group of recipes covered many of the antipatterns and design smell issues that can occur when using Optional. Now it’s time to explore different ways to use Optional with Stream APIs, as well as some other important aspects of Optional.

Recipe 18: Do not confuse Optional ofNullable() and of() functionality. Both the ofNullable() and of() methods are creational Optional static methods and are responsible for creating an Optional instance with a wrapped value in different ways. Of course, the empty() method creates an empty Optional instance describing the absence of a value. However, developers sometimes mix up ofNullable() and of(), which leads to unpleasant situations.

Use the of() method to create an Optional with a non-null value, because otherwise it would throw a null pointer exception immediately if the value passed is null. In that case, nothing will be created:


Employee emp = new Employee(1, "Mohamed Taman", "Business Development");       
Optional<Employee> employee = Optional.of(emp);

Use the ofNullable() method to create an Optional with a value that may or may not be null, and then return an empty Optional if the value is null. Otherwise it returns an Optional wrapping the original passed value:


Employee emp = service.getEmployee(int id); // emp maybe null
Optional<Employee> employee = Optional.ofNullable(emp);

Recipe 19: It is better to use numeric Optional over generic Optional for numbers. Sometimes you need to box primitive values inside Optional. Use it like this:


Optional<Integer> price = Optional.of(156);
Optional<Long> peopleCount = Optional.of(156_978_934_24L);
Optional<Double> finalPrice = Optional.of(230.17d);

Note that autoboxing hurts performance in some cases. Thus, it is better to use a numeric version of Optional that is a wrapper for the primitive values such as int, long, and double and that can be unwrapped with getAsInt(), getAsLong(), and getAsDouble() as appropriate:


OptionalInt price = OptionalInt.of(156);
OptionalLong peopleCount = OptionalLong.of(156_978_934_24L);
OptionalDouble finalPrice = OptionalDouble.of(230.17d);

API note: The OptionalInt, OptionalLong, and OptionalDouble classes have been around since Java 8.

Recipe 20: Use the filter() method to filter a wrapped value. The Optional class has a filter() method, which is just like the same method in the Stream API. Imagine a debit bank account that allows negative values for overdrafts:


//Check accountId for null and not being empty and follow account# schema
Optional<Account> accountFiltered = getAccount(accountId);

if (accountFiltered.isPresent()) {
    Account account = accountFiltered.get();
    if (!account.isOverdraftAllowed()) {
        throw new IllegalStateException("Overdraft is not allowed for this account.");
    }
    
    account.addToBalance(value);
    updateAccount(account);
}

Use the filter() method to check a predefined rule upon encountering a wrapped value. If the value is present, then filter() will return an Optional describing the value if the condition matched. Otherwise, it will return an empty Optional. Using this, convert the previous implementation to be more elegant and clearer:


getAccount(accountId)
        .filter(account -> account.isOverdraftAllowed())
        .ifPresentOrElse(account -> {
            account.addToBalance(amount);
            updateAccount(account);
        }, () -> {
            throw new IllegalStateException("Overdraft is not allowed for this account.");
        });

API note: The filter() method has existed since Java 8.

Recipe 21: Know when to use map() and flatMap() for extracting and transforming values. Sometimes the application needs to transform wrapped Optional values from one form to another by applying some transformation mapping functions that work on a wrapped value and then return the new result. For that, use the map() and flatMap() methods. They both perform transformations, but differently.

Let’s consider an Employee class that has a String getAddress() method. The application wants to search for an employee by ID. If the employee exists, get the employee’s address if it is not null. If the address is not blank, trim the address, convert it to uppercase letters, and return the result. This code does the talk:


var finalAddress = "No address found.";

Optional<Employee> optEmployee = employeeRepository.getEmployeeBy("1284");

if (optEmployee.isPresent()) {
    Employee employee = optEmployee.get();
    if (employee.getAddress() != null && !employee.getAddress().isBlank()) {
        finalAddress = employee.getAddress().trim().toUpperCase();
    }
}
System.out.println(finalAddress); 

Alternatively, use the map() function with Optional<Employee> returned from the search getEmployeeBy(String id) method to achieve the same functionality through more readable and clearer code:


1 var finalAddress = employeeRepository.getEmployeeBy(1284)
2        .map(Employee::getAddress)
3        .filter(Predicate.not(String::isBlank))
4        .map(String::trim)
5        .map(String::toUpperCase)
6        .orElse("No Address found.");

The map function (at line 2) does two things: It transforms the wrapped String value to Optional<String> only if the employee ID exists and the address is not null. Otherwise, the code at line 6 is applied and returns No Address found as a result.

However, if the employee and address exist, the code uses the filter method (at line 3) to check if the address is not blank. After that, the map function trims the address (at line 4), wraps it with Optional as a result, and then applies the final map function to convert the address to uppercase wrapped within Optional. Neat, huh!

Now, let’s reconsider that for the Employee class to have the getAddress() method but return Optional<String> and not the address as String, it’s necessary to try to avoid null from the beginning. And let’s apply the same requirements. Using the same code will create a problem, because the map function at line 2 will return a nested Optional<Optional<String>>. That is very smelly. To fix this problem, just change map() at line 2 into flatMap(), and everything should work like a charm:


1 var finalAddress = employeeRepository.getEmployeeBy(1284)
2        .flatMap(Employee::getAddress)
3 ...

A general rule of thumb: When the mapping function returns an Optional, use the flatMap() method instead of map() to get the flattened result from your Optional.

API note: The Optional class’s map() and flatMap() methods have existed since Java 8. The Predicate.not() and String.isBlank() methods have been here since Java 11.

Recipe 22: Can the code chain the Optional and Stream APIs? Sometimes the application needs to do the following:


public Optional<Employee> getEmployeeBy(String id) {
    return Optional.ofNullable(...);
}

public List<Employee> getEmployeesBy(List<String> ids) {
    return ids.stream()
            .map(this::getEmployeeBy)
            .filter(Optional::isPresent)
            .map(Optional::get)
            .collect(Collectors.toList());
}

Get the employees list for a given list of employee IDs, and return only employees who are present. Then use map() to produce Stream<Employee> and collect them to the final list. This involves a lot of conversation with the Stream API. Streamline this conversation by hooking the Optional API into the Stream API with the help of Optional.stream(), which allows replacing filter() and map() with the flatMap() method:


public List<Employee> getEmployeesBy1(List<String> ids) {
    return ids.stream()
            .map(this::getEmployeeBy)
            .flatMap(Optional::stream)
            .collect(Collectors.toList());
}

API note: The Optional.stream() method has existed since Java 9.

Recipe 23: Final thoughts on the Optional class. If you would like to test the equality of two Optional entities by using ==, or synchronize Optional objects, or run identity hash code checks, such as the following:


Optional<String> first = Optional.of("Java is turning 25");
Optional<String> second = Optional.of("Java is turning 25");

System.out.println(first == second); // return false
//or
synchronized(first){
    ...
}

Please don’t. You should avoid running any identity-sensitive operations on Optional, because Optional is a value-based class. Running identity-sensitive instances of Optional might have unpredictable results and should be avoided.

Finally, there’s no need to unwrap Optional values to check equality, as in the following:


if (first.get().equals(second.get())) … ; // return true

But do this:


if (first.equals(second)); // return true

This code is applicable because Optional.equals() compares the wrapped values, not the Optional objects. Why? Because Optional is a value-based class.

Conclusion

Using Optional in the right way isn’t as straightforward as it may seem.

This article delved deeply into some of the most common Optional class antipatterns and design smells and explained how to avoid them. I have provided several examples with alternative solutions to help you avoid such issues. Further, the article addressed the common confusion about mixing up Optional.of() and Optional.ofNullable() because such errors can lead to unpleasant results.

Finally, the article examined more-advanced recipes that deal with Stream API and Optional API chaining, transformations of Optional wrapped values, and conditions when it’s best to use map() and flatMap(). And I shared some final thoughts about avoiding any identity-sensitive operations on the value-based Optional class.

To learn more, see the Java SE 15 documentation for Optional, the book Java 8 in Action by Raoul-Gabriel Urma, Mario Fusco, and Alan Mycroft (Manning, 2014), and the presentation “Do you use the Optional class as it should be?

Mohamed Taman

Mohamed Taman (@_tamanm) is the CEO of SiriusXI Innovations and a Chief Solutions Architect for Effortel Telecommunications. He is based in Belgrade, Serbia, and is a Java Champion, and Oracle Groundbreaker, a JCP member, and a member of the Adopt-a-Spec program for Jakarta EE and Adopt-a-JSR for OpenJDK.

Share this Page