繁体   English   中英

java - 在Java Stream API中,中间操作延迟执行而终端操作急切执行是什么意思?

[英]What does it mean intermediate operations are lazily executed whereas terminal operations are eagerly executed in java Stream API?

list.stream().filter( a-> a < 20 && a > 7).forEach(a -> System.out.println(a));

fiter被懒惰地执行。

forEach被急切地执行。

这意味着什么?

假设您进行了以下操作。

list.stream()
    .map(a -> a * a)
    .filter(a -> a > 0 && a < 100)
    .map(a -> -a)
    .forEach(a -> System.out.println(a));

中间操作是映射和过滤器,终端操作是forEach 如果急切地执行中间操作,那么.map(a -> a * a)将立即映射整个流,结果将传递给.filter(a -> a > 0 && a < 10)它将立即过滤结果,然后将传递给.map(a -> -a) ,后者将映射过滤后的结果,然后将其传递给forEach ,然后它会立即打印流中的每个元素。

然而,中间操作不是急切的,而是懒惰的。 这意味着序列

list.stream()
    .map(a -> a * a)
    .filter(a -> a > 0 && a < 100)
    .map(a -> -a)

实际上并没有立即做任何事情。 它只是创建一个新的流来记住它应该执行的操作,但直到实际产生结果时才真正执行它们。 直到forEach尝试从流中读取一个值,然后它才会转到原始流,获取一个值,使用a -> a * a映射它,过滤它,如果它通过过滤器,则使用a -> -a映射它a -> -a然后将该值传递给forEach

这就像在餐厅工作的人被赋予了从脏堆中取出所有盘子,清洗它们,将它们堆叠起来,然后在厨师准备上菜时将它们交给厨师的工作。 人急了,就立刻把整堆脏盘子拿起来,一下子洗干净,叠好,等厨子要盘子时,就一个一个递过来上菜。

然而,懒惰的员工会意识到厨师一次只需要一个盘子,而且只有在食物准备好时才需要。 因此,当厨师需要一个盘子时,员工只需从一堆盘子中取出一个盘子,清洗干净并递给厨师,一个接一个,直到所有的盘子都洗干净,所有的食物都端上来为止。

那么有什么好处呢?

一个主要的优点是懒惰的方法大大改善了延迟。 您可能知道,程序的单个线程一次只能做一件事。 将这个比喻进一步扩展一下,想象一下大约有 800 个盘子,但厨师实际上必须等待洗衣机洗完盘子,然后再将一个递给他。 如果热心的洗碗工坚持先把盘子洗干净再递过来,厨师就得等着800个盘子都洗干净了,然后一次上菜800顿,到时候愤怒的顾客都已经离开了。

然而,有了懒惰的洗衣机,厨师每上菜,他只需要等一个盘子。 因此,如果洗盘子需要 10 秒并且服务几乎是即时的,那么在场景 1 中,所有餐点都会立即提供,但必须等待两个多小时。 但在场景 2 中,每顿饭的供应间隔约为 10 秒。 因此,即使提供所有餐点所需的时间相同,但场景 2 肯定更可取。

我在此将类比扩展了一点,但希望这可以帮助您更好地理解它。

StreamJavaDoc说:

流是懒惰的; 对源数据的计算只在终端操作启动时进行,源元素只在需要时被消费。

关于中间操作的JavaDoc

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

由于map是一个惰性操作,因此以下代码将不打印任何内容:

Stream.of(1, 2, 3).map(i -> {
    System.out.println(i);
    return i;
});

Stream缺少将执行它的终端操作,该操作将调用中间操作。

类似的list.stream().filter( a-> a > 20 && a < 7)将返回一个Streamlist元素还没有被过滤。

但即使执行了终端操作,还有更多关于懒惰的问题:

懒惰还可以避免在不必要时检查所有数据; 对于诸如“查找第一个长度超过 1000 个字符的字符串”之类的操作

如果需要执行惰性操作来确定Stream的结果,则会执行惰性操作。 并非来自源的所有元素都必须由惰性操作处理。

关于终端操作的 JavaDoc:

在几乎所有情况下,终端操作都是急切的,在返回之前完成对数据源的遍历和管道的处理。

此外,只能在Stream上应用一个终端操作。

执行完终端操作后,流管道被认为已消耗,不能再使用;

继续这个例子:

long count = Stream.of(1, 2, 3).map(i -> {
    System.out.println(i);
    return i;
}).count();

为了确定count ,映射是无关紧要的。 因此,此代码仍然不会打印任何内容。 但由于count()是一个终端操作,流被处理并且count获得分配的值3

如果我们将终端操作改为.min(Comparator.naturalOrder()); 然后执行所有映射,我们将看到打印的整数。

延迟执行意味着操作只会在必要时执行。

急切执行意味着操作将立即执行。

那么你可能会问什么时候执行惰性中间操作?

当对管道应用终端操作(Eager 操作)时。

那么我们如何知道一个操作是中间的(懒惰的)还是终端的(急切的)呢?

当操作返回Stream<T>其中T可以是任何类型时,它就是一个中间操作(懒惰); 如果操作返回任何其他内容,即 void、int、boolean 等,那么它是终端(急切)操作。

好的,这就是其他人指出的整个流链的样子。

Stream<Integer> s = Stream.of(1, 2, 3).map(i -> {
            System.out.println(i);
            return i;
        });

您可以将此流传递给不同线程上的任何方法并调用任何终端操作,然后将执行此映射。

Collection -> Stream -> (map) -> (filter) -> (map) -> collect(terminal)

当我还是一个新手时,当我们已经调用了一个方法时,很难理解它将如何执行。 在后台,当您调用 map 时,流 API 会创建一个委托,该委托将在稍后的某个时间点被调用。 当您一个接一个地调用操作时,它会在内部不断创建一个委托链。 该链基本上是一个双向链表。 现在,当您调用任何终端操作时,借助已创建的 DLL 中前一个指针,它会遍历前一个节点,直到遇到空值(调用的第一个操作)。 正是在那个时刻,它开始按顺序调用每个委托函数。 在内部,每个操作都表示为 StateLessOP 或 StatefulOP。 发生的事情是这样的(虽然我已经简化了它),

node.operation.execute() -> node = node.next -> ..
node.operation.execute() ..... ...

在这里,操作是最初创建的委托。

我现在将创建类似流的热切实现的东西。

public interface IChain<Type> {

    <OutType> IChain<OutType> map(ActionFunction<Type,OutType> f);

}

public class Chain<T> implements IChain<T> {

    private final T source;
    private int depth;
    private Chain prev;
    private Chain next;

    public Chain(T object)
    {
        this.source = object;
        this.depth = 0;
        this.prev = this.next = null;
    }

    public Chain(T object, Chain<?> chain) {
        this.source = object;
        this.prev = chain;
        this.prev.next = this;
        this.depth = this.prev.depth + 1;
    }

    // It will result in eager execution of the propagation chain.
    @Override
    public <OutType> IChain<OutType> map(ActionFunction<T, OutType> f) {
        return new Chain<>(f.execute(source),this);
    }
}



public interface ActionFunction<IN, OUT> {

    OUT execute(IN in);
}

要使用这个,

Chain<?> c = (Chain<?>) new Chain<String>("Test String").map(s -> {
         ArrayList<String> list = new ArrayList<>();

         for(int i = 0; i<100 ; i++) {
             list.add(s);
         }
         return list;
     }).map(strings -> new StringBuilder(strings.get(0)));

在这里,每个 map 函数都不会等待任何终端操作发生。 它会立即发生。 PS:代码没有任何意义。 只是为了解释这个概念。

希望这可以帮助。

这意味着list.stream().filter( a-> a > 20 && a < 7)不会开始执行,直到终端操作(例如forEach(a -> System.out.println(a)) )是应用于流。

这具有重要的性能影响,因为如果没有对流应用终端操作,则过滤它(或为此应用任何非终端操作)不会浪费资源。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM