In this post we are going to cover working with the Optional class introduced in Java 8. The introduction of Optional was new only to Java. Guava has had a version of Optional and Scala has had the Option type for some time. Here’s a description of Optional from the Java API docs:

A container object which may or may not contain a non-null value.

Since so many others have done a good job of describing the Optional type, we won’t be doing so here. Rather for this post, we are going to cover how to use the Optional type without resorting to directly accessing the value contained or doing explicit checks if a value is present.

Working with Optionals

Using the Optional class helps, to a large degree, prevent NullPointerExceptions. This is accomplished by allowing developers to more clearly define where missing values are to be expected. But at some point we’ll need to work with those potential values. The Optional class provides two methods that fit the bill right away: theOptional.isPresent() and Optional.get() methods. But our goal today is to work with Optional types and avoid (or at least delay until absolutely neccessary) methods that directly query or access the potential value contained within.

Conditional Operations

Java developers are used to writing something like the following:

List<String> values = new ArrayList<>;
String someString = getValue();
if(someString != null){
    values.add(someString)
}

Now instead we can use the Optional.ifPresent(Consumer<? super T> consumer) method. If the value is present the provided Consumer will be invoked with the value contained as a parameter. Here’s an example presented in a unit test:

    @Test
    public void optional_is_present_add_to_list_without_get_test() {
        List<String> words = Lists.newArrayList();

        Optional<String> month = Optional.of("October");
        Optional<String> nothing = Optional.ofNullable(null);

        month.ifPresent(words::add);
        nothing.ifPresent(words::add);

        assertThat(words.size(), is(1));
        assertThat(words.get(0), is("October"));
    }

From above, if the value is present it will be added to the list via a List.add() call, otherwise the operation is skipped.

Conditionally Changing Optional Values

Another common scenario is to check if a value is not null before we transform it. Conisider this very simple example:

String longString = getValue();
String smallerWord = null;
if(longString != null){
    smallerWord = longString.subString(0,4)
}

To transform the value of an Optional we can use the map method. Here’s an example, again in the context of a unit test:

    @Test
    public void optional_map_substring_test() {
        Optional<String> number = Optional.of("longword");
        Optional<String> noNumber = Optional.empty();

        Optional<String> smallerWord = number.map(s -> s.substring(0,4));
        Optional<String> nothing = noNumber.map(s -> s.substring(0,4));

        assertThat(smallerWord.get(), is("long"));
        assertThat(nothing.isPresent(), is(false));
    }

While this is a trivial example, we perform the mapping operation without explicitly checking if a value is present or retrieving the value to apply the given function. We simply provide the function and allow the API to handle the details. There also is an Optional.flatMap function. We use the Optional.flatMap method when we have an existing Optional and want to apply a function that also returns an a type of Optional. For example:

    @Test
    public void optional_flat_map_test() {
        Function<String, Optional<String>> upperCaseOptionalString = s -> (s == null) ? Optional.empty() : Optional.of(s.toUpperCase());

        Optional<String> word = Optional.of("apple");

        Optional<Optional<String>> optionalOfOptional = word.map(upperCaseOptionalString);

        Optional<String> upperCasedOptional = word.flatMap(upperCaseOptionalString);

        assertThat(optionalOfOptional.get().get(), is("APPLE"));

        assertThat(upperCasedOptional.get(), is("APPLE"));

    }

By using the flatMap method we can avoid the awkward return type of Option<Option<String>> by “flattening” out the results into a single Optional container.

Filtering Optionals

There is also the ability to filter Optional objects. If the value is present and matches the given predicate, the Optional is returned with it’s value intact, otherwise an empty Optional is returned. Here’s an exmaple of the Optional.filter method:

    @Test
    public void optional_filter_test() {
        Optional<Integer> numberOptional = Optional.of(10);
        Optional<Integer> filteredOut = numberOptional.filter(n -> n > 100);
        Optional<Integer> notFiltered = numberOptional.filter(n -> n < 100);

        assertThat(filteredOut.isPresent(), is(false));
        assertThat(notFiltered.isPresent(), is(true));
    }

Specifying Default Values

At some point we’ll want to retieve the value contained within an Optional, but if it’s not found, provide a default value instead. But instead of resorting to the “if not present then get” pattern, we can specify default values. There are two methods that allow for setting default values Oprional.orElse and Optional.orElseGet. With Optional.orElse we direclty supply the default value and with Optional.orElseGet we provide a Supplier that is used to provide the default value.

    @Test
    public void optional_or_else_and_or_else_get_test() {
        String defaultValue = "DEFAULT";

        Supplier<TestObject> testObjectSupplier = () -> {
            //Mimics set up needed to create object
            //Pretend these values need to be retrieved from some datastore
            String name = "name";
            String category = "justCreated";
            return new TestObject(name, category, new Date());

        };

        Optional<String> emptyOptional = Optional.empty();
        Optional<TestObject> emptyTestObject = Optional.empty();

        assertThat(emptyOptional.orElse(defaultValue), is(defaultValue));

        TestObject testObject = emptyTestObject.orElseGet(testObjectSupplier);

        assertNotNull(testObject);
        assertThat(testObject.category, is("justCreated"));
    }

When a Null/Absent Value is not Expected

For those cases where a missing value represents an error condition there is the Optional.orElseThrow method:

    @Test(expected = IllegalStateException.class)
    public void optional_or_else_throw_test() {
        Optional<String> shouldNotBeEmpty = Optional.empty();

        shouldNotBeEmpty.orElseThrow(() -> new IllegalStateException("This should not happen!!!"));
    }

Working with Collections of Optionals

Finally, let’s cover how we could use these methods in conjunction with Collections of Optional instances. It would be nice if we could specify a Collector that simply returned the values found or values plus defaults. Just for fun let’s define one:

 public static <T> Collector<Optional<T>, List<T>, List<T>> optionalToList() {
    return optionalValuesList((l, v) -> v.ifPresent(l::add));
 }


public static <T> Collector<Optional<T>, List<T>, List<T>> optionalToList(T defaultValue) {
   return optionalValuesList((l, v) -> l.add(v.orElse(defaultValue)));
}

private static <T> Collector<Optional<T>, List<T>, List<T>> optionalValuesList(BiConsumer<List<T>, Optional<T>> accumulator) {
  Supplier<List<T>> supplier = ArrayList::new;
  BinaryOperator<List<T>> combiner = (l1, l2) -> {
            l1.addAll(l2);
            return l1;
        };
   Function<List<T>, List<T>> finisher = l1 -> l1;
return Collector.of(supplier, accumulator, combiner, finisher);

}    

Here we’ve defined a Collector that returns a List containing the values found in a Collection of Optional types. There is also an overloaded version where we can specify a default value. Let’s wrap this up with a unit test showing the new Collector in action:

//some details left out for clarity

private List<String> listWithNulls = Arrays.asList("foo", null, "bar", "baz", null);
private List<Optional<String>> optionals;

@Before
public void setUp() {
  optionals = listWithNulls.stream().map(Optional::ofNullable).collect(Collectors.toList());
}

@Test
public void collect_optional_values_test(){
   List<String> upperCasedWords = optionals.stream().map(o -> o.map(String::toUpperCase)).collect(CustomCollectors.optionalToList());
   List<String> expectedWords = Arrays.asList("FOO","BAR","BAZ");

   assertThat(upperCasedWords,is(expectedWords));
}

@Test
public void collect_optional_with_default_values_test(){
  String defaultValue = "MISSING";

  List<String> upperCasedWords = optionals.stream().map(o -> o.map(String::toUpperCase)).collect(CustomCollectors.optionalToList(defaultValue));
  List<String> expectedWords = Arrays.asList("FOO", defaultValue, "BAR", "BAZ", defaultValue);

   assertThat(upperCasedWords,is(expectedWords));
}

In the first test, a List containing only the present values is returned, and in the second test there are defualt values as specified by the given parameter.

Conclusion

We’ve covered how to use the methods found in the Optional class. By making use of methods demonstrated here, working with missing data is bound to be easier and less error prone.

Resources