Java8流操作执行顺序

cgfeq70w  于 2021-07-13  发布在  Java
关注(0)|答案(5)|浏览(377)
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),操作,所以为什么输出不是像我解释的那样呢?

0s7z1bwu

0s7z1bwu1#

这是中间流操作的延迟执行/评估的结果。
操作链是按照从 collect()filter() ,值在上一步生成后立即被每个步骤消耗。
为了更清楚地描述发生了什么:
唯一的终端操作 collect() 开始评估链。 limit() 开始评估其祖先 map() 开始评估其祖先 filter() 开始使用源流中的值 1 进行评估, 2 计算并生成第一个值 map() 使用其祖先返回的第一个值并生成一个值 limit() 消耗这个价值 collect() 收集第一个值 limit() 需要来自 map() 来源 map() 需要来自其祖先的另一个值 filter() 继续评估以产生另一个结果,并在评估之后 3 以及 4 产生新的价值
4 map() 消耗它并产生新的价值 limit() 使用新值并返回它 collect() 收集最后一个值。
从java.util.stream文档:
流操作分为中间操作和终端操作,并结合起来形成流管道。流管道由源(如集合、数组、生成器函数或i/o通道)组成;后跟零个或多个中间操作,如stream.filter或stream.map;以及stream.foreach或stream.reduce等终端操作。
中间操作返回一个新的流。他们总是懒惰;执行诸如filter()之类的中间操作实际上并不执行任何过滤,而是创建一个新的流,在遍历时,该流包含与给定 predicate 匹配的初始流元素。在执行管道的终端操作之前,不会开始遍历管道源。

8iwquhpp

8iwquhpp2#

这个 Stream api并不意味着提供有关操作执行顺序的保证。这就是为什么你应该使用无副作用的功能。“短路”并没有改变任何事情,它只是关于不执行超过必要的操作(并且尽可能在有限的时间内完成,甚至对于无限的流源)。当你查看你的输出时,你会发现一切正常。执行的操作与您期望的操作匹配,结果也匹配。
只是顺序不匹配,这不是因为概念,而是你对实现的错误假设。但是,如果您考虑一个不使用中间存储的实现必须是什么样子的,您就会得出结论:它必须完全像所观察到的那样。一 Stream 将一个接一个地处理每个项目,在下一个项目之前对其进行过滤、Map和收集。

p1tboqfb

p1tboqfb3#

你注意到的行为是正确的。为了确定某个数字是否通过整个流管道,必须在所有管道步骤中运行该数字。

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

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

arknldoa

arknldoa4#

filter 以及 map 是中间操作。如文件所述:
中间操作返回一个新的流。他们总是懒惰;执行诸如filter()之类的中间操作实际上并不执行任何过滤,而是创建一个新的流,在遍历时,该流包含与给定 predicate 匹配的初始流元素。在执行管道的终端操作之前,不会开始遍历管道源。
[...]
缓慢地处理流可以显著提高效率;在上面的filter-map-sum示例这样的管道中,过滤、Map和求和可以融合到数据的一次传递中,中间状态最小。
所以当你调用终端操作(即 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;
 }
bpzcxfmw

bpzcxfmw5#

流是基于拉的。只有终端操作(如 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

这和你第一次打印出来的一致。

相关问题