• Tidak ada hasil yang ditemukan

Intermediate and Terminal Operations

Dalam dokumen Book Functional Programming in Java (Halaman 125-128)

Streams have two types of methods: intermediate and terminal, which work together. The secret behind their laziness is that we chain multiple interme- diate 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’re called. The cached behavior 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. Let’s look at an example to understand this better.

Suppose we’re given a collection of names and are asked to print in all caps the first name that is only three letters long. We can use Stream’s functional- style methods to achieve this. But first let’s create a few helper methods.

lazy/fpij/LazyStreams.java public class LazyStreams {

private static int length(final String name) { System.out.println("getting length for " + name);

return name.length();

}

private static String toUpper(final String name ) { System.out.println("converting to uppercase: " + name);

return name.toUpperCase();

} //...

}

The two helper methods simply print the parameters they receive before returning the expected results. We wrote these methods to take a peek at the intermediate operations in the code we’ll write next.

lazy/fpij/LazyStreams.java

public static void main(final String[] args) {

List<String> names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe",

"Mike", "Susan", "George", "Robert", "Julia", "Parker", "Benson");

final String firstNameWith3Letters = names.stream()

.filter(name -> length(name) == 3) .map(name -> toUpper(name)) .findFirst()

.get();

System.out.println(firstNameWith3Letters);

}

We started with a list of names, transformed it into a Stream, filtered out only names that are three 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 collec- tions, but it’s deceptively lazy; it didn’t do any more work than absolutely essential. Let’s take a look.

Method Evaluation Order

It would help to read the code from right to left, or bottom up, to see what’s really going on here. Each step in the call chain will do only enough work to Chapter 6. Being Lazy

112

ensure that the terminal operation in the chain completes. This behavior is in direct contrast to the usual eager evaluation, but is efficient.

If the code were eager, the filter() method would have first gone through all dozen names in the collection to create a list of two names, Kim and Joe, whose length is three (letters). The subsequent call to the map() method would have then evaluated the two names. The findFirst() method finally would have picked the first element of this reduced list. We can visualize this hypothetical eager order of evaluation in the next figure.

15 operations total 12 operations

2 operations

map 1 operation KIM

Figure 6—Hypothetical eager evaluation of operations

However, both the filter() and map() methods are lazy to the bone. As the execu- tion 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 order of evaluation is different as well, as we see in Figure 7, Actual lazy evaluation of operations, on page 114. The filter() method does not plow 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’s looking for.

5 operations total 3 operations 1 operation

map 1 operation KIM

Figure 7—Actual lazy evaluation of operations

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.

By examining the logic of this sequencing of operations, we can see that the execution will iterate over only essential elements in the collection. We can see evidence of this behavior by running the code.

getting length for Brad getting length for Kate getting length for Kim converting to uppercase: Kim KIM

From the output we can see that most of the elements in the example list were not evaluated once the candidate name we’re looking for was found.

The logical sequence of operations we saw in the previous example is achieved under the hood in the JDK using a fusing operation—all the functions in the intermediate operations are fused together into one function that is evaluated for each element, as appropriate, until the terminal operation is satisfied. In essence, there’s only one pass on the data—filtering, mapping, and selecting the element all happen in one shot.

Dalam dokumen Book Functional Programming in Java (Halaman 125-128)