Understanding Java 8 Optional

The purpose of the Optional class is to provide a type-level solution for representing optional values instead of null references.

Understanding Java 8 Optional
java optional

Hi there, in this article we will discuss usage of Optional class which was introduced in Java 8 and is defined as "a container object which may or may not contain a non-null value". Developers use Optionals in order to avoid null checking in places when executing a code leads to not a result, but to a null value and it results in NullPointerException. In such cases, Optional offers us some fancy functionality. Not all the features were introduced in Java 8, some features require Java 11.

Introducing Java Optional

The concept of Optional is not something new and has been already implemented in functional programming languages like Haskell or Scala. It proves to be very useful when modeling cases when a method call could return an uknown value or a value that doesn't exist (e.g. nulls).

The purpose of the Optional class is to provide a type-level solution for representing optional values instead of null references.

Creating Optional objects

First things first, we need to obtain an Optional's instance. There are several ways of creating Optional objects. To create an empty Optional object, we simply need to use its empty() static method:

Optional<Object> empty = Optional.empty();
System.out.println(empty); // Optional.empty

There are few methods inside of the Optional class, let's use them.

System.out.println(empty.isPresent()); // false
System.out.println(empty.isEmpty()); // true

We used the isPresent() method to check if there is a value inside the Optional object. A value is present only if we have created Optional with a non-null value and we used isEmpty() method as inverse and it gives us true as the value is empty.

We also have a static method of() on Optional class and let's create an Optional object with the static method of():

Optional<String> velocityBytes = Optional.of("velocityBytes");
System.out.println(velocityBytes.isPresent()); // true
System.out.println(velocityBytes.isEmpty()); // false
Optional<Integer> number = Optional.of(Integer.valueOf(29));
if (number.isPresent()) {
	System.out.println("Hoorayy! We have a value");
} else {
	System.out.println("No value");
}

You can note, that 29 is not an Integer, it is a container, that holds integer inside.

Optional makes us deal with nullable values explicitly as a way of enforcing good programming practices. In typical functional programming style, we can execute perform an action on an object that is actually present.

Optional<String> velocityBytes = Optional.of("velocityBytes");
velocityBytes.ifPresent(value -> System.out.println(value.length())); // 13

In the above code, we have one line to wrap the object into an Optional object and then perform implicit validation as well as execute the code.

However, the argument passed to the of() method can't be null. Otherwise, we'll get a NullPointerException.

But in case we expect some null values, we can use the ofNullable() method as shown below:

Optional<String> velocityBytes = Optional.ofNullable("velocityBytes");
System.out.println(velocityBytes.isPresent()); // true
System.out.println(velocityBytes.isEmpty()); // false

If we pass in a null reference, it returns an empty Optional and doesn't throw an exception.

Optional<String> velocityBytes = Optional.ofNullable(null);
System.out.println(velocityBytes.isPresent()); // true
System.out.println(velocityBytes.isEmpty()); // false

Default Value With orElse()

The orElse() method is used to get the value wrapped inside an Optional instance. It takes one parameter which acts as a default value. The orElse() method returns the wrapped value if it's present and its argument otherwise:

String name = (String) Optional.ofNullable(null).orElse("velocitybytes");
System.out.println(name); // velocitybytes

Default Value With orElseGet()

The orElseGet() method is similar to orElse(), however instead of taking a value to return if the Optional value is not present, it takes a supplier functional interface, which is invoked and return the value of the invocation. You can have complex logic inside it.

String name = Optional.ofNullable(null).orElseGet(() -> "velocitybytes");
System.out.println(name); // velocitybytes

We can use map() for transformation on the actual value inside of the Optional in case it's present.

String nameUpper = Optional.ofNullable("velocitybytes")
        .map(String::toUpperCase)
	    .orElse("website");
System.out.println(nameUpper);

Exceptions With orElseThrow()

The orElseThrow() method follows from orElse() and orElseGet() and adds a new approach for handling an absent value. Instead of returning a default value when the wrapped value is not present, it throws an exception:

String name = (String) Optional.ofNullable(null).orElseThrow(IllegalArgumentException::new);

Returning Value With get()

There is another approach to retrieve the wrapped value is the get() method:

Optional<String> velocityBytes = Optional.of("velocityBytes");
String name = velocityBytes.get();
System.out.println(name);

Unlike the previous approaches, get() can only return a value if the wrapped object is not null; otherwise it throws NoSuchElementException. This is major flaw of get() method.

Conditional return with filter()

The filter() method takes a predicate as an argument and returns an Optional object. If the wrapped value passes testing by the predicate, then the Optional is returned as-is. If the predicate returns false, then it will return an empty Optional.

public void whenOptionalFilterWorksThenCorrect() {
    Integer year = 2020;
    Optional<Integer> yearOptional = Optional.of(year);
    boolean is2020 = yearOptional.filter(y -> y == 2020).isPresent();
    System.out.println(is2020); // true
    boolean is2021 = yearOptional.filter(y -> y == 2021).isPresent();
    System.out.println(is2021); // false
}

The filter method is normally used this way to reject wrapped values based on a predefined rule. Say we want to buy a phone and we are worried about the price of it:

@Getter
@Setter
@AllArgsConstructor
public class Phone {
    private Double price;
}

Let's say we feed these objects to some code and its purpose is to check the price of phone within our budget range. We will see without Optional first and then see with Optional.

public boolean priceIsInRangeWithoutOptional(Phone phone) {
    boolean isInRange = false;

    if (phone != null && phone.getPrice() != null
        && (phone.getPrice() >= 8000 && phone.getPrice() >= 12000)) {
        isInRange = true;
    }
    return isInRange;
}

See how much code we have to write to achieve this, especially in the if condition. The only part of the if condition that is important is the price range check at last. Now let's see with Optional.

public boolean priceIsInRangeWithOptional(Phone phone) {
    return Optional.ofNullable(phone)
	        .map(Phone::getPrice)
	        .filter(p -> p >= 8000)
 	        .filter(p -> p <= 15000)
	        .isPresent();
}

The map call is simply used to transform a value to some other value, This doesn't modify the original value. If a null object is passed to this method, we don't expect any problem. The only logic we wrote is price check, Optional takes care of the rest.

Transforming Value With map()

We have seen how to accept or reject a value based on filter. We can use similar syntax to transform the Optional value with the map() method.

List<String> programmingLanguages = Arrays.asList(
	"Java", "Python", "C", "Go", "JavaScript"
);
Optional<List<String>> listOptional = Optional.of(programmingLanguages);
int size = listOptional
            .map(List::size)
            .orElse(0);
System.out.println(size);

Here, we wrapped a list of strings inside an Optional object and used its map method to perform an acttion on the contained list. The action we performed is to retrieve the size of the list. The map method returns the result of the computation wrapped inside Optional. We then have to call an appropriate method on the returned Optioal to retrieve its value.

Notice that the filter method simply performs a check on the value and returns a boolean. The map method however takes the existing value, performs a computation using this value, and returns the result of the computation wrapped in an Optional object.

Optional<String> nameOptional = Optional.of("velocitybytes");

int len = nameOptional
            .map(String::length)
            .orElse(0);
System.out.println(len);

We can chain map and filter together to do more:

Let's say we want to check the username of a particular user. 

String username = " velocitybytes";
Optional<String> usernameOptional = Optional.of(username);

boolean correctUsername = usernameOptinoal
            .filter(uname -> uname.equals("velocitybytes"))
            .isPresent();
System.out.println(correctUsername);

correctUsername = usernameOptional
           .map(String::trim)
           .filter(uname -> uname.equals("velocitybytes"))
           .isPresent();
System.out.println(correctUsername);

As we can see, without first cleaning the input, it will be filtered out. So, we transform a dirty username into a clean one with a map before filtering out incorrect ones.