Java 8流操作执行顺序

20
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),进行操作,那么为什么输出的结果不像我解释的那样呢?


9
为什么“必须”像第二个那样?因为它不应该(也确实不是这样)。 - Mark Rotteveel
5个回答

33

流是基于拉取的。只有终端操作(比如collect)才会导致项目被消耗。

从概念上讲,这意味着collect将从limit请求一个项目,limitmap请求一个项目,mapfilter请求一个项目,filter从流中请求一个项目。

按照图示,你问题中的代码导致

collect
  limit (0)
    map
      filter
        stream (returns 1)
      /filter (false)
      filter
        stream (returns 2)
      /filter (true)
    /map (returns 4)
  /limit (1)
  limit (1)
    map
      filter
        stream (returns 3)
      /filter (false)
      filter
        stream (returns 4)
      /filter (true)
    /map (returns 16)
  /limit (2)
  limit (2)
  /limit (no more items; limit reached)
/collect

这符合您的第一个打印输出。


你知道有哪些工具能够将基于流的代码转换为这样的输出吗?我尝试提出了一个相关问题,但被认为不是主题。 - Matt Passell
@MattPassell 不好意思,不行。 - Mark Rotteveel
@MarkRotteveel,由于您的回答和@Umberto的回答不一致,请确认collect方法是在limit达到2后调用还是每次调用limit时都会被调用。我已经问过他了,但还没有收到回复... - lupchiazoem
1
@San 我在我的回答中写道:“只有终端操作(例如collect)才会导致项目被消耗。”。仅有一个限制调用,我回答中的架构是为了说明概念,该架构中的 limit(0) 等不是方法调用,它表示对流进行 collect 方法调用时当前已经使用的项目数量。方法 limit(n) 是一种中间操作,它将返回一个最多发出n个项的流。如果需要更多,请考虑提出新问题并提供上下文链接。 - Mark Rotteveel
在San IOW中,每个中间操作都将调用它的流转换为另一个流。只有终端操作会从流中消耗项目,并且在这样做时,每个中间流都将从“上游”获取一个或多个项目。因此,collect会从limit流请求一个项目,该流将从map流请求一个项目,该流将从filter流请求一个项目,然后从原始stream请求两个项目(第一个不通过过滤器,所以是两个),依此类推,直到达到了limit的最大项目数(或者“上游”流已经没有项目)。 - Mark Rotteveel

16

这是中间流操作惰性执行/求值的结果。

操作链从collect()filter()以相反的顺序进行惰性求值,每个步骤会消费前一步骤生成的值。

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

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

来自java.util.stream文档

流操作分为中间操作和终端操作,并组合成流管道。流管道由源(例如Collection、数组、生成器函数或I/O通道);接下来是零个或多个中间操作,例如Stream.filter或Stream.map;以及一个终端操作,例如Stream.forEach或Stream.reduce。

终端操作比如Stream.forEach或Stream.reduce。
中间操作返回一个新的流。它们总是懒惰的;执行中间操作,例如filter()并不实际执行任何过滤,而是创建一个新的流,遍历该流时包含符合给定谓词的初始流的元素。管道源的遍历直到管道的终端操作执行才会开始。

1
由于limit还没有达到其2的限制,步骤8会被调用吗?换句话说,在步骤7之后,collect操作是否会保留第一个值。 - lupchiazoem
各位,FYI:有关粗体摘录文本的良好解释可以在Stream and lazy evaluation帖子的答案中找到。我的懒想法。 - lupchiazoem

4
Stream API并不保证操作的执行顺序,因此您应该使用无副作用的函数。这就是为什么要使用无副作用函数的原因。 "短路" 不会改变任何东西,它只是不执行不必要的操作(甚至对于无限流源也可以在有限时间内完成)。当您查看输出时,您会发现一切正常。执行的操作与您预期的操作相匹配,结果也是如此。

唯一不匹配的是顺序,这不是由于 概念,而是由于您对 实现 的错误假设。但如果您考虑不使用中间存储的实现应该是什么样子,您会得出结论:一个 Stream 将处理每个项目,一个接一个地过滤、映射和收集它,然后才进行下一个项目。


3

您注意到的行为是正确的。为了确定一个数字是否通过整个流水线,您必须将该数字通过所有流水线步骤运行。

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

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


2

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

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

[...]

延迟处理流可以实现显著的效率;在像上面的filter-map-sum示例这样的管道中,过滤、映射和求和可以融合成单个数据通道的一次传递,中间状态最小。

因此,当您调用终端操作(即collect())时,可以将其视为类似于以下内容(这真的很简化(您将使用收集器来累积管道的内容,流不可迭代,...),不能编译,但这只是为了可视化事物):

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;
 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接