Java Optional Class

https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html

What is it?

  1. A container object which may or may not contain a non-null value. If a value is present, isPresent() will return true and get() will return the value.
  2. Additional methods that depend on the presence or absence of a contained value are provided, such as orElse() (return a default value if value not present) and ifPresent() (execute a block of code if the value is present).
  3. It is part of java.util package.
static Optional<String> changeCase(String word) {
    if (name != null && word.startsWith("A")) {
        return Optional.of(word.toUpperCase());
    }
    else {
        return Optional.ofNullable(word); // someString can be null
    }
}

Useful methods

The Optional class provides useful methods to help us work with that API. The important ones for this article are the of(), orElse(), and empty() methods:

of(T value) returns an instance of an Optional with a value inside orElse(T other) returns the value inside an Optional, otherwise returns other Finally, empty() returns an empty instance of Optional

What are the advantages of using the Optional class?

Below are the main advantage of using the Optional class:

It encapsulates optional values, i.e., null or not-null values, which helps in avoiding null checks, which results in better, readable, and robust code. It acts as a wrapper around the object and returns an object instead of a value, which can be used to avoid run-time NullPointerExceptions.

Optional vs. null

Java allows us to use the getName() method from the object returned by findById() without a null check. In that case, we can only discover the problem at runtime.

It is mandatory to treat the Optional properly at compile time. With that, we can mitigate unexpected errors at runtime.

Consider this class

public class UserRepositoryWithNull {

    private final List<User> dbUsers = Arrays.asList(new User("1", "John"), new User("2", "Maria"), new User("3", "Daniel"));

    public User findById(String id) {

        for (User u : dbUsers) {
            if (u.getId().equals(id)) {
                return u;
            }
        }

        return null;
    }
}

The null scenario must be handled by the developers.

@Test
public void givenNonExistentUserId_whenSearchForUser_andNoNullCheck_thenThrowException() {

    UserRepositoryWithNull userRepositoryWithNull = new UserRepositoryWithNull();
    String nonExistentUserId = "4";

    assertThrows(NullPointerException.class, () -> {
        System.out.println("User name: " + userRepositoryWithNull.findById(nonExistentUserId)
          .getName());
    });
}

Writing the Repository with Optional

public class UserRepositoryWithOptional {

    private final List<User> dbUsers = Arrays.asList(new User("1", "John"), new User("2", "Maria"), new User("3", "Daniel"));

    public Optional<User> findById(String id) {

        for (User u : dbUsers) {
            if (u.getId().equals(id)) {
                return Optional.of(u);
            }
        }

        return Optional.empty();
    }
}

Handling Optional

@Test
public void givenNonExistentUserId_whenSearchForUser_thenOptionalShouldBeTreatedProperly() {

    UserRepositoryWithOptional userRepositoryWithOptional = new UserRepositoryWithOptional();
    String nonExistentUserId = "4";

    String userName = userRepositoryWithOptional.findById(nonExistentUserId)
      .orElse(new User("0", "admin"))
      .getName();

    assertEquals("admin", userName);
}

Design Clear Intention APIs

Optional in the return of a method provides a clear intention of what we should expect from that method: it returns something or nothing.

Declarative Programming

Optional class gives developers the ability to use a chain of fluent methods. It provides a “pseudo-stream” similar to the stream() from collections, but with only one value. That means we can call methods like map(), and filter() on the value in it. That helps to create more declarative programs, instead of imperative ones.

imperative

@Test
public void givenExistentUserId_whenFoundUserWithNameStartingWithMInRepositoryUsingNull_thenNameShouldBeUpperCased() {

    UserRepositoryWithNull userRepositoryWithNull = new UserRepositoryWithNull();

    User user = userRepositoryWithNull.findById("2");
    String upperCasedName = "";

    if (user != null) {
        if (user.getName().startsWith("M")) {
            upperCasedName = user.getName().toUpperCase();
        }
    }

    assertEquals("MARIA", upperCasedName);
}

declarative

@Test
public void givenExistentUserId_whenFoundUserWithNameStartingWithMInRepositoryUsingOptional_thenNameShouldBeUpperCased() {

    UserRepositoryWithOptional userRepositoryWithOptional = new UserRepositoryWithOptional();

    String upperCasedName = userRepositoryWithOptional.findById("2")
      .filter(u -> u.getName().startsWith("M"))
      .map(u -> u.getName().toUpperCase())
      .orElse("");

    assertEquals("MARIA", upperCasedName);
}

The imperative way needs two nested if statements to check if the object is not null and filter the user name. If the User is not found, the uppercase string remains empty.

In the declarative way, we use lambda expressions to filter the name and map the uppercase function to the User found. If the User is not found, we return an empty string using orElse().

It’s still a matter of preference which one we use. Both of them achieve the same result. The imperative way needs a bit more digging to understand what the code means. If we add more logic in the first or second if statement, for example, it might create some confusion about what the intention of that code is. In this type of scenario, declarative programming makes it clear what is the intent of the code: to return the uppercased name, otherwise, return an empty string.

References

  1. https://www.baeldung.com/java-optional-uses

Links to this note