Chaining Optionals using flatMap and map

The blog post How I learnt to like Optional shows an example of how to chain Optional values safely, using Optional.orElse() to provide empty objects for the next level in the chain.

This blog post uses Optional.flatMap() and Optional.map(), to achieve the same thing, without having to create the empty objects, and without doing any evaluation for empty objects.

To recap, the chained references with defaults for the next level, looks like this:

public Integer findLeafnodeValue() {
    return someService.findSomething().orElse(new Something())
        .findOther().orElse(new Other())
        .findNestedOther().orElse(new NestedOther())
        .someValue();
}

This works, but has two obvious downsides:

  1. The verbosity
  2. The empty objects used as defaults are always created, whether they are needed or not

An obvious fix would be to switch from .orElse() to .orElseGet(), and put the creation of default values into lambdas:

public Integer findLeafnodeValue() {
    return someService.findSomething().orElseGet(() -> new Something())
        .findOther().orElseGet(() -> new Other())
        .findNestedOther().orElseGet(() -> new NestedOther())
        .someValue();
}

This works for delaying creation of defaults until they are actually needed, but it makes the code even more verbose.

And the added verbosity is for code that won’t actually do anything useful, which is slightly annoying.

It’s possible to switch from lambdas to method references for the constructors, and that makes the code less verbose:

public Integer findLeafnodeValue() {
    return someService.findSomething().orElseGet(Something::new)
        .findOther().orElseGet(Other::new)
        .findNestedOther().orElseGet(NestedOther::new)
        .someValue();
}

Method references are also slightly more efficient than lambdas (one less level of method call in the call stack, and no overhead for initial runtime creation of the lambda).

But it’s still a verbose way of creating code that ideally shouldn’t be run. And objects are created and thrown away just to return an empty or null result.

So the question is: is there a way to chain objects that doesn’t require the empty default objects to be created.

The answer is: “Yes, there is, using flatMap() and/or map()“:

public Integer findLeafnodeValue() {
    return someService.findSomething()
        .flatMap(s -> s.findOther())
        .flatMap(o -> o.findNestedOther())
        .map(n -> n.someValue())
        .orElse(null);
}

What happens here, is that the lambdas are executed, if the Optional the .flatMap() or .map() is called on, is non-empty, if not it just skips down to the next method in the chain, until it gets to the .orElse().

So if all of the Optionals in the chain are empty, the flatMap() and/or map() invocations do nothing, orElse is called on an empty Optional and the default value is returned (in this case, null).

Similarily, if the initial SomeService.findSomething() returns a non-empty Optional, and all of the lambdas result in values, .orElse() is called on an Optional holding the value of the last lambda, and .orElse(null) will return that value.

No dummy empty objects are called for the case with empty results. And no lambdas are called in this case either, which results in low overhead for the empty case.

Note that here also, since the methods called take no arguments, it is possible to use method references instead of lambdas:

public Integer findLeafnodeValue() {
    return someService.findSomething()
        .flatMap(Something::findOther)
        .flatMap(Other::findNestedOther)
        .map(NestedOther::someValue)
        .orElse(null);
}

Slightly more efficient, and arguably more readable.

The question that pops up, is: “Why map() at the last call?”.

Both map() and flatMap() are methods that take an incomprehensible, complicated, argument and returns Optional<U>, so what is the difference between them?

The reason map() being used on the last call, is that NestedOther.someValue() returns Integer and not Optional<Integer>.

If NestedOther.someValue() had returned Optional<Integer>, the method would have looked like this:

public Integer findLeafnodeValue() {
    return someService.findSomething()
        .flatMap(Something::findOther)
        .flatMap(Other::findNestedOther)
        .flatMap(NestedOther::someValue)
        .orElse(null);
}

Similarily, if map() had been used instead of flatMap() the code would have needed to look like this (switching back to using lambdas):

public Integer findLeafnodeValue() {
    return someService.findSomething()
        .map(s -> s.findOther())
        .map(o -> o.get().findNestedOther())
        .map(n -> n.get().someValue())
        .orElse(null);
}

Both map() and flatMap() are expected to return an Optional<U>, but flatMap() returns the value of the mapping function as-is, while map() wraps the result of the mapping function in an Optional<>.

I.e. if the result of the mapping function already is Optional<Something>, the result becomes Optional<Optional<Something>>, which isn’t very useful (see the previous code example), so then flatMap() is the right method to use.

But if the result is e.g. an Integer and what’s needed to complete the reference chain, is an Optional<Integer>, then map() is the right method.

So in conclusion: If the API returns Optional values for all returned results that may be empty, it is possible to traverse an hierarchical data structure safely, to retrieve a leaf value, without checking for if intermediate values are present, without the overhead of creating empty objects, and without the overhead of evaluating unneccessary code when the traversal fails.

It’s even possible to terminate an optional flatMap()/map() chain with .orElseThrow() if that’s what’s desired.

One possible downside to throwing exceptions from the map()/flatMap() approach, compared to the approach in How I learnt to like Optional, is that it isn’t possible to throw an exception pinpointing exactly which step of the data structure traversal failed.

2 thoughts on “Chaining Optionals using flatMap and map”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.