Understanding, Accepting and Leveraging Optional in Java

By: Eugen
  |  March 11, 2024
Understanding, Accepting and Leveraging Optional in Java

Overview

One of the most interesting features that Java 8 introduces to the language is the new Optional class. The main issue this class is intended to tackle is the infamous NullPointerException that every Java programmer knows only too well.

Essentially, this is a wrapper class that contains an optional value, meaning it can either contain an object or it can simply be empty.

Optional comes along with a strong move towards functional programming in Java and is meant to help in that paradigm, but definitely also outside of that.

Let’s start with a simple use-case. Before Java 8, any number of operations involving accessing an object’s methods or properties could result in a NullPointerException:

String isocode = user.getAddress().getCountry().getIsocode().toUpperCase();

If we wanted to make sure we won’t hit the exception in this short example, we would need to do explicit checks for every value before accessing it:

if (user != null) {
    Address address = user.getAddress();
    if (address != null) {
        Country country = address.getCountry();
        if (country != null) {
            String isocode = country.getIsocode();
            if (isocode != null) {
                isocode = isocode.toUpperCase();
            }
        }
    }
}

As you can see, this can easily become cumbersome and hard to maintain.

To ease this process, let’s take a look at how we can use the Optional class instead, from creating and verifying an instance, to using the different methods it provides and combining it with other methods that return the same type, the latter being where the true power of Optional lies.

Creating Optional Instances

To reiterate, an object of this type can contain a value or be empty. You can create an empty Optional by using the method with the same name:

@Test(expected = NoSuchElementException.class)
public void whenCreateEmptyOptional_thenNull() {
    Optional<User> emptyOpt = Optional.empty();
    emptyOpt.get();
}

Not surprisingly, attempting to access the value of the emptyOpt variable results in a NoSuchElementException.

To create an Optional object that can contain a value – you can use the of() and ofNullable() methods. The difference between the two is that the of() method will throw a NullPointerException if you pass it a null value as an argument:

@Test(expected = NullPointerException.class)
public void whenCreateOfEmptyOptional_thenNullPointerException() {
    Optional<User> opt = Optional.of(user);
}

As you can see, we’re not completely rid of the NullPointerException. For this reason, you should only use of() when you are sure the object is not null.

If the object can be both null or not-null, then you should instead choose the ofNullable() method:

Optional<User> opt = Optional.ofNullable(user);

Accessing the Value of Optional Objects

One way to retrieve the actual object inside the Optional instance is to use the get() method:

@Test
public void whenCreateOfNullableOptional_thenOk() {
    String name = "John";
    Optional<String> opt = Optional.ofNullable(name);
    
    assertEquals("John", opt.get());
}

However, as you saw before, this method throws an exception in case the value is null. To avoid this exception, you can choose to first verify if a value is present or not:

@Test
public void whenCheckIfPresent_thenOk() {
    User user = new User("[email protected]", "1234");
    Optional<User> opt = Optional.ofNullable(user);
    assertTrue(opt.isPresent());
    assertEquals(user.getEmail(), opt.get().getEmail());
}

Another option for checking the presence of a value is the ifPresent() method. In addition to performing the check, this method also takes a Consumer argument and executes the lambda expression if the object is not empty:

opt.ifPresent( u -> assertEquals(user.getEmail(), u.getEmail()));

In this example, the assertion is only executed if the user object is not null.

Next, let’s look at ways in which alternatives for empty values can be provided.

Returning Default Values

The Optional class provides APIs for returning the value of the object or a default value if the object is empty.

The first method you can use for this purpose is orElse(), which works in a very straight-forward way: it returns the value if it’s present, or the argument it receives if not:

@Test
public void whenEmptyValue_thenReturnDefault() {
    User user = null;
    User user2 = new User("[email protected]", "1234");
    User result = Optional.ofNullable(user).orElse(user2);
    assertEquals(user2.getEmail(), result.getEmail());
}

Here, the user object was null, so user2 was returned as a default instead.

If the initial value of the object is not null, then the default value is ignored:

@Test
public void whenValueNotNull_thenIgnoreDefault() {
    User user = new User("[email protected]","1234");
    User user2 = new User("[email protected]", "1234");
    User result = Optional.ofNullable(user).orElse(user2);
    assertEquals("[email protected]", result.getEmail());
}

The second API in the same category is orElseGet() – which behaves in a slightly different manner. In this case, the method returns the value if one is present, and if not it executes the Supplier functional interface that it receives as an argument, and returns the result of that execution:

User result = Optional.ofNullable(user).orElseGet( () -> user2);

Difference between orElse() and orElseGet()

At first glance, it might seem as if the two methods have the same effect. However, this is not exactly the case. Let’s create some examples that highlight the similarity as well as the difference in behavior between the two.

First, let’s see how they behave when an object is empty:

@Test
public void givenEmptyValue_whenCompare_thenOk() {
    User user = null
    logger.debug("Using orElse");
    User result = Optional.ofNullable(user).orElse(createNewUser());
    logger.debug("Using orElseGet");
    User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}
private User createNewUser() {
    logger.debug("Creating New User");
    return new User("[email protected]", "1234");
}

In the code above, both methods call the createNewUser() method which logs a message and returns a User object.

The output of this code is:

Using orElse
Creating New User
Using orElseGet
Creating New User

Therefore, when the object is empty and the default object is returned instead, there is no difference in behavior.

Next, let’s take a look at a similar example in which the Optional is not empty:

@Test
public void givenPresentValue_whenCompare_thenOk() {
    User user = new User("[email protected]", "1234");
    logger.info("Using orElse");
    User result = Optional.ofNullable(user).orElse(createNewUser());
    logger.info("Using orElseGet");
    User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}

The output this time is:

Using orElse
Creating New User
Using orElseGet

Here, both Optional objects contain a non-null value which the methods will return. However, the orElse() method will still create the default User object. By contrast, the orElseGet() method will no longer create a User object.

This difference can have a significant effect on performance if the operation executed involves more intensive calls, such as a web service call or a database query.

Returning an Exception

Next to the orElse() and orElseGet() methods, Optional also defines an orElseThrow() API – which instead of returning an alternate value, throws an exception instead if the object is empty:

@Test(expected = IllegalArgumentException.class)
public void whenThrowException_thenOk() {
    User result = Optional.ofNullable(user)
      .orElseThrow( () -> new IllegalArgumentException());
}

Here, if the user value is null, an IllegalArgumentException is thrown.

This allows us to have a lot more flexible semantics and decide the exception that gets thrown instead of always seeing a NullPointerException.

Now that we have a good understanding of how we can leverage Optional by itself, let’s have a look at additional methods that can be used to apply transformations and filtering to Optional values.

Transforming Values

Optional values can be transformed in a number of ways; let’s start with map() and flatMap() methods.

First, let’s see an example that uses the map() API:

@Test
public void whenMap_thenOk() {
    User user = new User("[email protected]", "1234");
    String email = Optional.ofNullable(user)
      .map(u -> u.getEmail()).orElse("[email protected]");
    
    assertEquals(email, user.getEmail());
}

map() applies the Function argument to the value, then returns the result wrapped in an Optional. This makes it possible to apply and chain further operations on the response – such orElse() here.

By comparison, flatMap() also takes a Function argument that is applied to an Optional value, and then returns the result directly.

To see this in action, let’s add a method that returns an Optional to the User class:

public class User {    
    private String position;
    public Optional<String> getPosition() {
        return Optional.ofNullable(position);
    }
    
    //...
}

Since the getter method returns an Optional of String value, you can use as the argument for flatMap(), where this is called for an Optional User object. The return will be the unwrapped String value:

@Test
public void whenFlatMap_thenOk() {
    User user = new User("[email protected]", "1234");
    user.setPosition("Developer");
    String position = Optional.ofNullable(user)
      .flatMap(u -> u.getPosition()).orElse("default");
    
    assertEquals(position, user.getPosition().get());
}

Filtering Values

Alongside transforming the values, the Optional class also offers the possibility to “filter” them based on a condition.

The filter() method takes a Predicate as an argument and returns the value as it is if the test evaluates to true. Otherwise, if the test is false, the returned value is an empty Optional.

Let’s see an example of accepting or rejecting a User based on a very basic email verification:

@Test
public void whenFilter_thenOk() {
    User user = new User("[email protected]", "1234");
    Optional<User> result = Optional.ofNullable(user)
      .filter(u -> u.getEmail() != null && u.getEmail().contains("@"));
    
    assertTrue(result.isPresent());
}

The result object will contain a non-null value as a result of it passing the filter test.

Chaining Methods of the Optional class

For more powerful uses of Optional, you can also chain different combinations of most of its methods, given that most of them return objects of the same type.

Let’s rewrite the example in the introduction using Optional.

First, let’s refactor the classes so that the getter methods return Optional references:

public class User {
    private Address address;
    public Optional<Address> getAddress() {
        return Optional.ofNullable(address);
    }
    // ...
}
public class Address {
    private Country country;
    
    public Optional<Country> getCountry() {
        return Optional.ofNullable(country);
    }
    // ...
}

The structure above can be visually represented as a nested set:

The structure above can be visually represented as a nested set

Now you can remove the null checks and use the Optional methods instead:

@Test
public void whenChaining_thenOk() {
    User user = new User("[email protected]", "1234");
    String result = Optional.ofNullable(user)
      .flatMap(u -> u.getAddress())
      .flatMap(a -> a.getCountry())
      .map(c -> c.getIsocode())
      .orElse("default");
    assertEquals(result, "default");
}

The code above can be further reduced by using method references:

String result = Optional.ofNullable(user)
  .flatMap(User::getAddress)
  .flatMap(Address::getCountry)
  .map(Country::getIsocode)
  .orElse("default");

As a result, the code looks much cleaner than our early cumbersome, conditional-driven version.

Java 9 Additions

Next to the features introduced in Java 8, Java 9 adds three more methods to the Optional class: or(), ifPresentOrElse() and stream().

The or() method is similar to orElse() and orElseGet() in the sense that it provides alternate behavior if the object is empty. In this case, the returned value is another Optional object that is produced by a Supplier argument.

If the object does contain a value, then the lambda expression is not executed:

@Test
public void whenEmptyOptional_thenGetValueFromOr() {
    User result = Optional.ofNullable(user)
      .or( () -> Optional.of(new User("default","1234"))).get();
                 
    assertEquals(result.getEmail(), "default");
}

In the example above, if the user variable is null, then an Optional containing a User object with the email “default” is returned.

The ifPresentOrElse() method takes two arguments: a Consumer and a Runnable. If the object contains a value, then the Consumer action is executed; otherwise, the Runnable action is performed.

This method can be useful if you want to perform an action using the value if one is present, or simply keep track of whether a value was defined or not:

Optional.ofNullable(user).ifPresentOrElse( u -> logger.info("User is:" + u.getEmail()),
  () -> logger.info("User not found"));

Lastly, the new stream() method allows you to benefit from the extensive Stream API by transforming the instance to a Stream object. This will be an empty Stream if no value is present, or a Stream containing a single value – in case the Optional contains a non-null value.

Let’s see an example of processing an Optional as a Stream:

@Test
public void whenGetStream_thenOk() {
    User user = new User("[email protected]", "1234");
    List<String> emails = Optional.ofNullable(user)
      .stream()
      .filter(u -> u.getEmail() != null && u.getEmail().contains("@"))
      .map( u -> u.getEmail())
      .collect(Collectors.toList());
   
    assertTrue(emails.size() == 1);
    assertEquals(emails.get(0), user.getEmail());
}

Here the use of a Stream makes it possible to apply the Stream interface methods filter(), map() and collect() to obtain a List.

How Should Optional Be Used

There are a few things to consider when using Optional, to determine when and how it should be used.

One note of importance is that Optional is not Serializable. For that reason, it’s not intended to be used as a field in a class.

If you do need to serialize an object that contains an Optional value, the Jackson library provides support for treating Optionals as ordinary objects. What this means is that Jackson treats empty objects as null and objects with a value as fields containing that value. This functionality can be found in the jackson-modules-java8 project.

Another situation when it’s not very helpful to use the type is as a parameter for methods or constructors. This would lead to code that is unnecessarily complicated:

User user = new User("[email protected]", "1234", Optional.empty());

Instead, it’s much easier to use method overloading to handle parameter which aren’t mandatory.

The intended use of Optional is mainly as a return type. After obtaining an instance of this type, you can extract the value if it’s present or provide an alternate behavior if it’s not.

One very useful use-case of the Optional class is combining it with streams or other methods that return an Optional value to build fluent APIs.

Let’s see an example of using the Stream findFirst() method which returns an Optional object:

@Test
public void whenEmptyStream_thenReturnDefaultOptional() {
    List<User> users = new ArrayList<>();
    User user = users.stream().findFirst().orElse(new User("default", "1234"));
    
    assertEquals(user.getEmail(), "default");
}

Conclusion

Optional is a useful addition to the Java language, intended to minimize the number of NullPointerExceptions in your code, though not able to completely remove them.

It’s also a well designed and very natural addition to the new functional support added in Java 8.

Overall, this simple yet powerful class helps create code that’s, simply put, more readable and less error-prone than its procedural counterpart.

Interested in continuously improving your Java application?
Try our free dynamic code profiler, Prefix and our full lifecycle APM, Retrace. 

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]