在Java Stream API中,中间操作是延迟执行的,而终端操作是急切执行的,这意味着什么?

时间:2018-10-22 09:37:13

标签: java java-stream

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

fiter被懒惰地执行。

forEach被迫执行。

那是什么意思?

5 个答案:

答案 0 :(得分:3)

假设您进行了以下操作。

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将其传递给forEach

这就像某人在一家餐馆里工作,他的工作是从肮脏的堆中取出所有盘子,洗净,堆放起来,然后在准备提供食物时交给厨师。如果此人渴望,他们将立即拿起整堆脏盘子,将它们全部洗净,然后堆放起来,然后当厨师想要这些盘子时,他将它们一块一块递下来供食用。

但是,一个懒惰的员工会意识到,厨师一次只需要一个盘子,而且只有在准备好食物时才需要。因此,当厨师需要一盘时,员工只需从堆中拿出一个盘,将其洗净并交给厨师,一步一步地将其洗净,直到所有食物都盛满为止。

那有什么优势?

一个主要优点是,惰性方法大大提高了延迟。您可能已经知道,程序的单个线程一次只能做一件事。进一步扩展类比,想象一下大约有800个盘子,但是厨师实际上不得不等待洗衣机洗完盘子再交给他。如果急切的洗衣机坚持要在交出任何盘子之前先洗净所有盘子,那么厨师将不得不等待800个盘子全部洗净,然后立即提供800顿饭,这时所有愤怒的顾客都会离开。 >

但是,对于懒惰的洗衣机,厨师要提供的每顿饭,他只需要等待一盘即可。因此,如果洗盘子需要10秒钟并且几乎是立即上菜,那么在方案1中,所有餐点都可以立即上菜,但要等两个多小时。但是在方案2中,每顿饭相隔约10秒。因此,即使花相同的时间来提供所有餐点,情况2当然还是更可取的。

在这里我将类推范围缩小了一些,但是希望这可以帮助您更好地理解它。

答案 1 :(得分:2)

Stream的{​​{3}}说:

  

流很懒;仅对源数据进行计算   当启动终端操作时,源元素为   仅根据需要食用。

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)将返回Stream,但尚未过滤list中的任何元素。

但是即使执行了终端操作,关于懒惰的更多信息:

  

懒惰还可以避免在没有数据时检查所有数据   必要;对于诸如“ ”的操作,找到第一个字符串的时间长于   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());,则将执行所有映射,然后将看到打印的整数。

答案 2 :(得分:2)

懒惰地执行意味着仅在必要时执行该操作

迅速执行意味着操作将立即执行

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

在管道上执行了终端操作(急切操作)时。

那么我们怎么知道操作是中间的(懒惰的)还是终端的(急切的)?

当该操作返回Stream<T>时,其中T可以是任何类型,则它是一个中间操作(惰性);如果该操作返回了其他任何内容,例如void,int,boolean等,则为终端操作(急切)。

答案 3 :(得分:1)

好的,所以这就是整个流链的外观,就像其他人指出的那样。

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

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

收藏->流->(地图)->(过滤器)->(地图)->收集(终端)

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

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

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

我现在将创建类似流的急切实现。

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:代码没有任何意义。只是为了解释这个概念。

希望这会有所帮助。

答案 4 :(得分:0)

这意味着list.stream().filter( a-> a > 20 && a < 7)在流上执行终端操作(例如forEach(a -> System.out.println(a)))之前不会开始执行。

这具有重要的性能含义,因为如果没有在流上应用任何终端操作,那么在过滤它(或对此应用任何非终端操作)时,将不会浪费资源。