这里是记录学习这本书 Functional Programming in Java: Harnessing the Power Of Java 8 Lambda Expressions 的读书笔记,如有侵权,请联系删除。
Leveraging the Laziness of Streams
好处
可以推迟一连串的求值,这样每次只有逻辑中最重要的部分被求值
The lazy evaluation of Streams is quite powerful. First, we don’t have to do anything special to derive their benefits. In fact, we’ve used them many times already! Second, they can postpone not just one, but a sequence of evaluations so that only the most essential parts of the logic are evaluated, and only when needed.
中间业务和终点站业务(终端操作):
laziness背后的秘密是,把多个中间操作连在一起,然后再进行终端操作。
The secret behind their laziness is that we chain multiple intermediate
operations followed by a terminal operation.
像map()和filter()这样的方法是中间性的;对它们的调用会立即返回,提供给它们的lambda表达式不会立即被评估。这些方法的核心行为被缓存起来,以便以后执行,当它们被调用时,没有真正的工作被完成。
只有当终端操作之一,如findFirst()和reduce(),被调用时,缓存的核心行为才会被执行,但只足以产生所需的结果。
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. Only when one of the terminal operations, like findFirst() and reduce(), is called are the cached core behaviors executed, but only enough to produce the desired result.
假设我们得到了一组名字,并被要求用大写字母打印第一个只有三个字母长的名字。
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.
两个需要的方法,求名字长度和变成大写
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();
}
下面这一段代码干了啥呢?
我们从一个人名列表开始,将其转化为Stream,只过滤掉长度为三个字母的人名,将选定的人名转化为全大写,然后从这组人名中挑选第一个人名。
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.
public static void main(final String[] args) {
List<String> names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe",
"Mike", "Susan", "George", "Robert", "Julia", "Parker", "Benson");
System.out.println("//" + "START:CHAIN_OUTPUT");
{
final String firstNameWith3Letters =
names.stream()
.filter(name -> length(name) == 3)
.map(name -> toUpper(name))
.findFirst()
.get();
System.out.println(firstNameWith3Letters);
}
System.out.println("//" + "END:CHAIN_OUTPUT");
}
从右到左,或从下往上阅读代码,会有助于了解这里的真实情况。
调用链中的每一步将只做足够的工作来确保链中的终端操作(terminal operation)完成。
这种行为与通常的急切评估(eager evaluation)直接相反,但却很有效率。
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 ensure that the terminal operation in the chain completes. This behavior is in direct contrast to the usual eager evaluation, but is efficient.
如果从前往后看,所需要执行的操作。
如果代码是急切的,filter()方法将首先穿过集合中的所有十几个名字,创建一个两个名字的列表,Kim和Joe,其长度为3(字母)。随后对map()方法的调用就会评估这两个名字。findFirst()方法最后会在这个缩小的列表中挑选出第一个名字。
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.
然而,filter()和map()方法都是懒到骨子里的。在执行过程中,filter()和map()方法会存储lambda表达式,并将一个façade传递给链中的下一个调用。只有当findFirst()这个终端操作被调用时,才开始评估。
However, both the filter() and map() methods are lazy to the bone. 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.
评估的顺序也是不同的,正如我们在下图中看到的。
filter()方法并不是一次就把集合中的所有元素都扫一遍。相反,它一直运行到找到第一个满足所附lambda表达式中的条件的元素。一旦找到一个元素,它就会将其传递给链中的下一个方法。
下一个方法,即本例中的map(),在给定的输入上做它的部分,并将其向下传递。
当评估到达终点时,终端操作会检查它是否已经收到了它所寻找的结果。
The order of evaluation is different as well, as we see in the next figure. 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.
如果终端操作得到了它所需要的东西,那么该链的计算就终止了。
如果终端操作没有被满足,它将要求对集合中的更多元素进行操作链。
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.
我们可以运行上述代码,产生我们所期望的结果,只评估了前三个名字:
//START:CHAIN_OUTPUT
getting length for Brad
getting length for Kate
getting length for Kim
converting to uppercase: Kim
KIM
//END:CHAIN_OUTPUT
我们在前面的例子中看到的逻辑操作顺序是在JDK中通过融合操作(fusing operation)实现的–所有中间操作的方法都被融合成一个方法,这个方法对每个元素进行适当的评估,直到终端操作(terminal operation)得到满足。
从本质上讲,对数据只有一次传递–过滤、映射和选择元素都是一次性完成的。
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.
我们把filter,map放在一起,而把终端操作findFirst单独放在后面。
System.out.println("//" + "START:SPLIT_OUTPUT");
{
Stream<String> namesWith3Letters =
names.stream()
.filter(name -> length(name) == 3)
.map(name -> toUpper(name));
System.out.println("Stream created, filtered, mapped...");
System.out.println("ready to call findFirst...");
final String firstNameWith3Letters =
namesWith3Letters.findFirst()
.get();
System.out.println(firstNameWith3Letters);
}
System.out.println("//" + "END:SPLIT_OUTPUT");
输出结果
//START:SPLIT_OUTPUT
Stream created, filtered, mapped...
ready to call findFirst...
getting length for Brad
getting length for Kate
getting length for Kim
converting to uppercase: Kim
KIM
//END:SPLIT_OUTPUT
从输出中我们可以清楚地看到,中间操作将其真正的工作推迟到最后一个负责任的时刻,即调用终端操作的时候。即使如此,它们也只做了满足终端操作所需的最小工作。
From the output we 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.