3.4.  Identify the lambda operations that are lazy

[Note]

Streams have two types of methods: intermediate and terminal, which work together. The secret behind their laziness is that we chain multiple intermediate operations followed by a terminal operation.

Methods like map() and filter() are intermediate; calls to them return immediately and the lambda expressions provided to them are not evaluated right away. The core behavior of these methods is cached for later execution and no real work is done when they are called. This code will not print anything to output:


List<String> names = Arrays.asList("Volha", "Ivan", "Daria", "Mikalai", "Anastasia");
names.stream()
    .filter(s -> {
        System.out.println("filtering " + s);
        return true;
    });

					

The cached behavior of intermediate operations is run when one of the terminal operations, like findFirst() and reduce(), is called. Not all the cached code is executed, however, and the computation will complete as soon as the desired result is found.

Suppose you are given a collection of names and are asked to print in all caps the first name that is only four letters long. You use Stream functional-style methods to achieve this:


List<String> names = Arrays.asList("Volha", "Ivan", "John", "Mike", "Alex");
String name = names.stream()
    .filter(s -> {
        System.out.println("filtering " + s);
        return s.length() == 4;
    })
    .map(s -> {
        System.out.println("uppercasing " + s);
        return s.toUpperCase();
    })
    .findFirst()
    .get();
System.out.println(name);

					

You started with a list of names, transformed it into a Stream, filtered out only names that are four letters long, converted the selected names to all caps, and picked the first name from that set.

At first glance it appears the code is doing a lot of work transforming collections, but it is lazy: it did not do any more work than absolutely essential. The output will be:

filtering Volha
filtering Ivan
uppercasing Ivan
IVAN
					

Both the filter() and map() methods are lazy. As the execution goes through the chain, the filter() and map() methods store the lambda expressions and pass on a façade to the next call in the chain. The evaluations start only when findFirst(), a terminal operation, is called.

The filter() method does not look through all the elements in the collection in one shot. Instead, it runs until it finds the first element that satisfies the condition given in the attached lambda expression. As soon as it finds an element, it passes that to the next method in the chain. This next method, map() in this example, does its part on the given input and passes it down the chain. When the evaluation reaches the end, the terminal operation checks to see if it has received the result it is looking for.

If the terminal operation got what it needed, the computation of the chain terminates. If the terminal operation is not satisfied, it will ask for the chain of operations to be carried out for more elements in the collection.

Writing the series of operations as a chain is the preferred and natural way in Java 8. But to really see that the lazy evaluations did not start until code reached the terminal operation, let’s break the chain from the previous code into steps:


List<String> names = Arrays.asList("Volha", "Ivan", "Daria", "John");
Stream<String> stream = names.stream()
    .filter(s -> {
        System.out.println("filtering " + s);
        return s.length() == 4;
    })
    .map(s -> {
        System.out.println("uppercasing " + s);
        return s.toUpperCase();
    });
System.out.println("Stream was filtered and mapped...");
String name = stream.findFirst().get();
System.out.println(name);

					

We transformed the collection into a stream, filtered the values, and then mapped the resulting collection. Then we printed some message, and then, separately, we called the terminal operation. The output is:

Stream was filtered and mapped...
filtering Volha
filtering Ivan
uppercasing Ivan
IVAN
					

From the output you can clearly see that the intermediate operations delayed their real work until the last responsible moment, when the terminal operation was invoked. And even then, they only did the minimum work necessary to satisfy the terminal operation.

Professional hosting         Free 'Oracle Certified Expert Web Services Developer 6' Guide     Exam 1Z0-810: Upgrade to Java SE 8 Programmer Quiz