首页 > 解决方案 > 流式传输多个过滤器内部

问题描述

我正在尝试了解 Java 的 Stream API 的内部调用。

我有以下代码,它有两个过滤器(中间)操作和一个终端操作。

IntStream.of(1,2,3)
    .filter(e->e%2==0)
    .filter(e->e==2)
    .forEach(e->System.out.println(e));

Stream -> 返回带有覆盖过滤器的 Stream -> 返回带有覆盖过滤器的 Stream -> 终端

我看到对于每个中间操作,都会返回一个带有重写filter方法的新流。一旦它到达终端方法,流就会执行filter. filter()如果有两个filter 操作而不是一次,我看到它会运行两次。

我想了解一个流遍历如何能够调用过滤器两次。

粘贴下面的 IntPipeline 代码,该代码针对 Stream 中的过滤器方法命中。

@Override
public final IntStream filter(IntPredicate predicate) {
    Objects.requireNonNull(predicate);
    return new StatelessOp<Integer>(this, StreamShape.INT_VALUE,
                                    StreamOpFlag.NOT_SIZED) {
        @Override
        Sink<Integer> opWrapSink(int flags, Sink<Integer> sink) {
            return new Sink.ChainedInt<Integer>(sink) {
                @Override
                public void begin(long size) {
                    downstream.begin(-1);
                }

                @Override
                public void accept(int t) {
                    if (predicate.test(t)) ///line 11
                        downstream.accept(t);
                }
            };
        }
    };
}

返回一个新的filter()Stream ,其谓词设置为e%2==0,然后再次返回一个新的 Stream ,其谓词为e==2。一旦终端操作被命中,对于每次遍历,谓词代码都会在第 11 行执行。

编辑:我看到它downstream用于将中间操作链接为 LinkedList。那么所有的实现都被添加到linkedlist之前的阶段并在遍历开始时调用?

标签: javajava-8java-stream

解决方案


我认为您对 Streams 的理解有一些负担和复杂的概念。

  1. 您的困惑与过滤器(或任何其他操作)实现无关;也不存在覆盖中间(或任何其他)流操作的概念;

    覆盖是一个完全不同的概念,它与继承有关

  2. 每个单独的流都是一个单独的管道,它有一个(1)开始,(2)可选的中间部分,以及(3)结束/结束

  3. 中间操作不会被覆盖;相反,它们形成一个顺序的操作链,流的每个元素都必须按照相同的顺序执行(除非在某个中间操作中丢弃了一个元素)。

    将 K 个对象的 Stream 视为一个管道,K 的实例将在其中通过。该管道具有开始/源(对象进入管道的位置)和结束/目的地;然而,在这两者之间,可能有几个中间操作将(或者)过滤、转换、装箱等这些对象。流是惰性的,这意味着在调用终端操作之前不会执行中间操作(这是流的一个很棒的特性),因此,当终端操作被调用时,每个项目,一次一个,将通过这个管道.

此外,请阅读此片段

为了执行计算,流操作被组合成流管道。流管道由源(可能是数组、集合、生成器函数、I/O 通道等)、零个或多个中间操作(将流转换为另一个流,例如filter(Predicate))和终端操作(产生结果或副作用,例如count()or forEach(Consumer))。流是懒惰的;仅在发起终端操作时才对源数据进行计算,并且仅在需要时消耗源元素。

记住范式,Stream 包括:

  1. Sourcer 操作- 从某些对象集合中获取流,或表示现有流对象(下图中的事务);
  2. 中间操作s - 可以是 0 或更多,并且需要在您的流上引入中间细粒度、调整或修改操作(下图中的filtersortedmap);
  3. 终端操作- 终止和关闭流。它可以生产(返回)一些东西,消费(接受)一些东西,两者都不是,或两者兼而有之(在下图中收集)。

在此处输入图像描述


如果您仍然感到困惑(现在不应该是这种情况),您还可以参考一些重要的观点,这些要点可能会更清楚地说明您的困惑:

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

懒惰地处理流可以提高效率;在诸如上面的 filter-map-sum 示例的管道中,过滤、映射和求和可以融合到数据的单次传递中,具有最小的中间状态。懒惰还允许在没有必要时避免检查所有数据。对于诸如“查找第一个超过 1000 个字符的字符串”之类的操作,只需检查足够多的字符串即可找到具有所需特征的字符串,而无需检查源中可用的所有字符串。(当输入流是无限的而不仅仅是大时,这种行为变得更加重要。)


推荐阅读