Java 8流操作执行顺序

时间:2015-04-28 09:31:52

标签: java collections java-8 java-stream

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> twoEvenSquares = numbers.stream().filter(n -> {
    System.out.println("filtering " + n);
    return n % 2 == 0;
}).map(n -> {
    System.out.println("mapping " + n);
    return n * n;
}).limit(2).collect(Collectors.toList());


for(Integer i : twoEvenSquares)
{
    System.out.println(i);
}

执行时输出的逻辑

filtering 1
filtering 2
mapping 2
filtering 3
filtering 4
mapping 4
4
16

如果流遵循短路概念(我们使用限制流操作),则输出必须如下所示:

filtering 1
filtering 2
filtering 3
filtering 4
mapping 2
mapping 4
4
16

因为在过滤2之后,我们还要找到另外一个元素来分层限制(2),操作,那么为什么输出不像我解释的那样?

5 个答案:

答案 0 :(得分:19)

Streams是基于拉式的。只有终端操作(如collect)才会导致使用项目。

从概念上讲,这意味着collect会询问来自limit的{​​{1}},limit和来自map的{​​{1}}的项目,来自流的map

示意图中您的问题中的代码会导致

filter

这符合你的第一次打印输出。

答案 1 :(得分:9)

这是中间流操作的延迟执行/评估的结果。

collect()filter()以相反的顺序对操作链进行延迟评估,每一步的值都会在上一步生成后立即消耗。

更清楚地描述正在发生的事情:

  1. 唯一的终端操作collect()开始评估链。
  2. limit()开始评估其祖先
  3. map()开始评估其祖先
  4. filter()开始使用源流中的值
  5. 评估
  6. 1,评估2并生成第一个值
  7. map()使用其祖先返回的第一个值并生成一个值
  8. limit()消耗该值
  9. collect()收集第一个值
  10. limit()需要map()来源
  11. 中的其他值
  12. map()需要其祖先的另一个值
  13. filter()恢复评估以产生另一个结果,并在评估34后生成新值4
  14. map()使用它并生成新值
  15. limit()使用新值并将其返回
  16. collect()收集最后一个值。
  17. 来自java.util.stream docs

      

    流操作分为中间和终端   操作,并组合形成流管道。一条小溪   管道由一个源(如Collection,一个数组,一个   生成器功能,或I / O通道);然后是零或更多   中间操作,如Stream.filter或Stream.map;和a   终端操作,如Stream.forEach或Stream.reduce。

         

    中间操作返回一个新流。 他们总是懒惰;   执行中间操作,例如filter()实际上并不执行   执行任何过滤,而是创建一个新的流,当时   遍历,包含匹配的初始流的元素   给出谓词。管道源的遍历直到开始   执行管道的终端操作

答案 2 :(得分:1)

您注意到的行为是正确的。为了找出一个数字是否通过整个Stream管道,您必须在所有管道步骤中运行该数字。

filtering 1 // 1 doesn't pass the filter
filtering 2 // 2 passes the filter, moves on to map
mapping 2 // 2 passes the map and limit steps and is added to output list
filtering 3 // 3 doesn't pass the filter
filtering 4 // 4 passes the filter, moves on to map 
mapping 4 // 4 passes the map and limit steps and is added to output list

现在管道可以结束,因为我们有两个数字通过了管道。

答案 3 :(得分:1)

filtermap是中间操作。正如文件所述:

  

中间操作返回一个新流。他们总是懒惰;   执行诸如filter()之类的中间操作实际上并不是这样   执行任何过滤,而是创建一个新的流,当时   遍历,包含匹配的初始流的元素   给出谓词。管道来源的遍历不会开始直到   管道的终端操作已执行

     

[...]

     

懒洋洋地处理流可以显着提高效率;在上面的filter-map-sum示例的管道中,过滤,映射和求和可以融合到数据的单个传递中,具有最小的中间状态。

所以当你调用你的终端操作(即collect())时,你可以想到这样的事情(这真的很简单(你将使用收集器来累积管道的内容, Streams不可迭代,...)并且不编译但只是为了可视化事物):

public List collectToList() {
    List list = new ArrayList();
    for(Elem e : this) {
        if(filter.test(e)) { //here you see the filter println
            e = mapping.apply(e); //here you see the mapping println
            list.add(e);
            if(limit >= list.size())
                break;
         }
     }
     return list;
 }

答案 4 :(得分:1)

Stream API并不意味着提供有关操作执行顺序的保证。这就是为什么你应该使用无副作用的功能。 “短路”并没有改变它的任何内容,只是没有执行超过必要的操作(并且在可能的有限时间内完成,即使对于无限流源也是如此)。当你看到你的输出时,你会发现一切正常。执行的操作与您期望的操作匹配,结果也是如此。

只有订单不匹配,这不是因为概念,而是你对实现的错误假设。但是如果你想一下不使用中间存储的实现如何看起来像,你会得出结论,它必须完全像观察。 Stream将逐个处理每个项目,过滤,映射并在下一个项目之前收集它。