javajava-streamlazy-evaluationeager

type of map() and filter() operations of Optional


Are the map() and filter() of Optional are lazy like Stream?

How can I confirm their type?


Solution

  • There is a fundamental difference between a Stream and an Optional.

    A Stream encapsulates an entire processing pipeline, gathering all operations before doing anything. This allows the implementation to pick up different processing strategies, depending on what result is actually requested. This also allows to insert modifiers like unordered() or parallel() into the chain, as at this point, nothing has been done so far, so we can alter the behavior of the subsequent actual processing.

    An extreme example is Stream.of(1, 2, 3).map(function).count(), which will not process function at all in Java 9, as the invariant result of 3 can be determined without.

    In contrast, an Optional is just a wrapper around a value (if not empty). Each operation will be performed immediately, to return either, a new Optional encapsulating the new value or an empty Optional. In Java 8, all methods returning an Optional, i.e.map, flatMap or filter, will just return an empty optional when being applied to an empty optional, so when chaining them, the empty optional becomes a kind of dead-end.

    But Java 9 will introduce Optional<T> or​(Supplier<? extends Optional<? extends T>>), which may return a non-empty optional from the supplier when being applied to an empty optional.

    Since an Optional represents a (possibly absent) value, rather than a processing pipeline, you can query the same Optional as many times you want, whether the query returns a new Optional or a final value.

    It’s easy to verify. The following code

    Optional<String> first=Optional.of("abc");
    Optional<String> second=first.map(s -> {
        System.out.println("Running map");
        return s + "def";
    });
    System.out.println("starting queries");
    System.out.println("first: "+(first.isPresent()? "has value": "is empty"));
    System.out.println("second: "+(second.isPresent()? "has value": "is empty"));
    second.map("second's value: "::concat).ifPresent(System.out::println);
    

    will print

    Running map
    starting queries
    first: has value
    second: has value
    second's value: abcdef
    

    demonstrating that the mapping function is evaluated immediately, before any other query, and that we still can query the first optional after we created a second via map and query optionals multiple times.

    In fact, it is strongly recommended to check via isPresent() first, before calling get().

    There is no equivalent stream code, as re-using Stream instances this way is invalid. But we can show that intermediate operations are not performed before the terminal operation has been commenced:

    Stream<String> stream=Stream.of("abc").map(s -> {
        System.out.println("Running map");
        return s + "def";
    });
    System.out.println("starting query");
    Optional<String> result = stream.findAny();
    System.out.println("result "+(result.isPresent()? "has value": "is empty"));
    result.map("result value: "::concat).ifPresent(System.out::println);
    

    will print

    starting query
    Running map
    result has value
    result value: abcdef
    

    showing that the mapping function is not evaluated before the terminal operation findAny() starts. Since we can’t query a stream multiple times, findAny() even uses Optional as return value, which allows us to do that with the final result.


    There are other semantic differences between the operations of the same name, e.g. Optional.map will return an empty Optional if the mapping function evaluates to null. For a stream, it makes no difference whether the function passed to map returns null or a non-null value (that’s why we can count the elements without knowing whether it does).