Introduction

The Optional class was added to the JDK from Java 8 onwards. It is a value based class meant to provide a better alternate mechanism for a method to indicate no result to the caller. In this post, we will look at the usage of Optional and what it solves (readers familiar with this can skip this part). Next, we will see some of the not-recommended practices around the usage of Optional.

A method with no return

Before Java 8, there was no way for a method to signal or inform the caller of that method that no result is available. Two options that existed were

  1. Throw an exception
  2. Return null

We will see the implementation of these. Let us say we want to lookup a product from a database and show the product description on the UI.

Throwing an exception to indicate no result

Following option 1, the method contract will look like

public Product lookupProduct(String productName) throws ProductNotFoundException

Here, the caller of the code can have an alternate course of action to take like displaying a default Not found message in place of the product description when no product is found. When we follow the method of throwing an exception, the caller of this code has to catch the ProductNotFoundException to take the alternate action.

public String getDisplayText(String productName) {
    try {
        return dao.lookupProduct(productName)
            .getProductDescription();
    } catch(ProductNotFoundException e) {
        return String.format("Product with name %s does not exist", productName);
    }
}

This violates the principle to not use exceptions to control the flow of the code.

(Side note: You could check on the previous post for the differences between a Checked exception and unchecked exception).

Imagine throwing an exception for all scenarios to indicate no result. The code base will be filled with try..catch statements rather than focusing on the business/domain logic.

Returning a null

This option is the widely used option before Java 8. The method signature will look like as shown below. The method returns null when a product is not found.

public Product lookupProduct(String productName)

This is only slightly better than the previous option. Now the caller has to have an if..else block to handle the missing products.

public String getDisplayText(String productName) {
    Product product = dao.lookupProduct(productName);
    if (product != null) {
        return product.getProductDescription();
    } else {
        return String.format("Product with name %s does not exist", productName);
    }
}

Using Optional to indicate a missing value

Optional is a container object that is used as a wrapper over some value. It might indicate either the presence or absence of a value. Two key methods of Optional are:

  • get(): It returns the value if present and throws NoSuchElementException when get is called on an optional that is empty
  • isPresent(): This is used to inspect the presence of a value. Returns true if the value is present and false otherwise.

The Database layer now returns an Optional when a product is not present.

public Optional<Product> lookupProduct(String productName)

Refactoring the the caller code, gives

public String getDisplayText(String productName) {
    Optional<Product> product = dao.lookupProduct(productName);
    if (product.isPresent()) {
        return product.get().
                getProductDescription();
    } else {
        return String.format("Product with name %s does not exist", productName);
    }
}

We check if a product object is present, wrapped within the Optional, using isPresent. If it is, then we access the underlying value using the get method.

If you think this looks no better than the variant using if..else checks, you are absolutely right. The level of complexity has slightly increased and also we need an additional wrapper object (Optional). The real simplification comes in using the other functional methods that exist to work on the optional value without checking for its existence.

Extracting and returning the Optional value

The methods we should use here are map and orElse.

  • map(Function<? super T, ? extends U> mapper): If a value is present (wrapped within the Optional), it applies the mapping function to it and returns the mapped value (whatever the mapping function returns)
  • orElse(T other): It returns the value of the Optional if present; else returns the value passed.

Another method similar to orElse is orElseGet. It takes a Supplier to produce the default value.

First, we have to use the map function to map a Product to its description. Next, use orElse passing a default value to it. When we call orElse on an Optional, it returns the value if present from the Optional or returns the passed default value.

public String getDisplayText(String productName) {
    Optional<Product> product = dao.lookupProduct(productName);
    return product
            .map(Product::getProductDescription)
            .orElse(String.format("Product with name %s does not exist", productName));
}

Isn’t this code self-explanatory and functional looking? We are passing a method reference Product::getProductDescription to the map method. In essence, it is a lambda expression product -> product.getProductDescription. Refer to the post on Method References if you are not familiar with it.

What if the mapped value is null

What if the product exists, but there is no description (it is null)? Both the code snippets we saw earlier (Option 1 using exception and Option 2 using null handling) would have returned a null from the getDisplayText method. To handle this, we should do an additional null check on the value returned by product.getProductDescription(). Yikes!!

When using Optional, if the function passed to a map returns a null, the map call returns an empty Optional.

Due to that the (Optional not having a value), it returns the default text passed to orElse as the result. This is the desired behaviour and we require no code change to achieve this.

An alternate approach is to use the filter method to filter the Optional. The filter takes a Predicate and returns the same Optional only if the predicate succeeds; else it returns an empty Optional.

return product
        .filter(p -> p.getProductDescription() != null)
        .map(Product::getProductDescription)
        .orElse(String.format("Product with name %s does not exist", productName));

But this would not buy us much here, as the map method takes care of null. It would be useful if you want to invoke some method on the returned product description string. Using a filter would only execute the later stages (map here) if the Optional is not empty and has a non-null product description.

In other words, if the predicate returns false, it returns an empty Optional. This results in skipping the subsequent stages or steps and the value returned by orElse will be returned.

Other Java Optional methods

empty()

Returns an empty Optional instance. Calling isPresent on the returned instance returns false.

of(T value)

Returns an Optional with the specified value. The value passed cannot be non-null. Calling isPresent on the returned instance returns true.

ofNullable(T value)

Use this variant if the value to be wrapped in an Optional can be null.

ifPresent(Consumer<? super T> consumer)

If the value if present, the consumer is invoked with that value.

orElseThrow(Supplier<? extends X> exceptionSupplier) throws X

This is another method to deal with missing values (like orElse and orElseGet). This is used to throw an exception when a value is not present.

//To print the optional product 
product.ifPresent(System.out::println)

//To throw a Runtime expception if product is not present
product.orElseThrow(() -> new RuntimeException(String.format("Product with name %s does not exist", productName)));

Java Optional - Gotchas and Misuses

Let us move on to look at some of the ways in which we use Optional that are not recommended. Before that let us see the difference between orElse and orElseGet.

orElse vs orElseGet

We already saw the usage of orElse to return a sensible default when Optional is empty. Now, orElseGet exists for the same purpose, but it uses a supplier to return that default. We could have written the above code using orElseGet as:

return product
        .map(Product::getProductDescription)
        .orElseGet(() -> String.format("Product with name %s does not exist", productName));

It would work perfectly. But, there is a particular usecase for orElseGet. It is meant to be executed lazily i.e., only when the Optional is empty. On the contrary, orElse is executed eagerly.

Let us tweak our example so that if a product is not available, we call a very slow database to get the product details. Assume it has a superset of products and hence it will always return a valid product (no need of optional).

public String getDisplayText2(String productName) {
    Optional<Product> product = dao.lookupProduct(productName);
    return product
            .map(Product::getProductDescription)
            .orElse(getProductDescriptionFromASlowDB(productName));
}

private String getProductDescriptionFromASlowDB(String productName) {
    //Simulating hitting a DB
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //return response;
    return "some-result";
}

If you run this code, you can see that getProductDescriptionFromASlowDB is always called!!. This is because the value we pass to orElse has to be the default, fully computed String result. Hence, it has to call the getProductDescriptionFromASlowDB method to evaluate the default result.

To make this call lazy (call getProductDescriptionFromASlowDB only when the lookupProduct call returns an empty Optional, use orElseGet.

return product
        .map(Product::getProductDescription)
        .orElseGet(() -> getProductDescriptionFromASlowDB(productName)); //lazy

Java 8 optional method parameters

Sometimes, you might have a method parameter that is optional. It is tempting to use an Optional for this.

public void doSomething(String str, Optional<Integer> i) {...}

But Optional was not made for this purpose and hence not recommended. It might make sense and look okay if the caller of this method is also working with an Optional<Integer> at hand and can just pass it in. If it is not the case, the callers have to pack the integer into an Optional (Optional.of(..)) to call this method which results in additional complexity for the calling code.

This is best accomplished in having two methods as

public void doSomething(String str, Optional<Integer> i) {
    doSomething(str, Optional.empty()); //delegate to the other method
}

public void doSomething(String str, Optional<Integer> i) {...}

Using Optional as instance variables

Another usage of Optional is to attempt to use it as an instance variable. Again, Optional was not intended for that. In my view, it is okay for the getter or the accessor to return an Optional, but not have the instance variable type as Optional.

public class Product {
  private String desciption;
  
  public Optional<String> getDescription() {
      return Optional.of(description);
  }
}

Intellij Idea IDE even complains about this

Reports any uses of java.util.Optional<T>, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLong or com.google.common.base.Optional as the type for a field or a parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result”. Using a field with type java.util.Optional is also problematic if the class needs to be Serializable, which java.util.Optional is not.

I recommend reading the article Daniel Olszewski on the Java 8 Optional best practices and wrong usage.

Conclusion

In this post, we learnt the usage of Optional in Java 8 and the methods it supports. Next, we looked at a gotchas of using orElse vs orElseGet. Finally, we looked at wrong usages of Optional as a method parameter and as an instance variable. The last part is subjective and debatable. In my view, I agree with the idea that Optional is to provide a mechanism to represent “no result” for a method. It wasn’t meant to be a general purpose replacement of null. If that had been the intention, it would have been built into the language as a feature and not as a value class.

Please do leave a comment to voice your opinion on this. I would be happy to hear them.