为什么我必须链接Java中的Stream操作?

时间:2018-11-12 02:04:45

标签: java java-8 java-stream

我认为我以某种方式研究的所有资源都强调一个流只能使用一次,而这种消耗是通过所谓的终端操作完成的(这对我很清楚)。

出于好奇,我尝试了以下方法:

Observable<any[]>

最终会引发运行时异常:

import java.util.stream.IntStream;

class App {
    public static void main(String[] args) {
        IntStream is = IntStream.of(1, 2, 3, 4);
        is.map(i -> i + 1);
        int sum = is.sum();
    }
}

这是很平常的事,我想念一些东西,但仍然想问:据我所知Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229) at java.util.stream.IntPipeline.reduce(IntPipeline.java:456) at java.util.stream.IntPipeline.sum(IntPipeline.java:414) at App.main(scratch.java:10) 是一个中间(和惰性)操作,并且在Stream上什么都不做本身。仅当调用终端操作map(这是一个急切的操作)时,Stream才会消耗

但是为什么我必须将它们链接起来?

有什么区别
sum

is.map(i -> i + 1);
is.sum();

3 个答案:

答案 0 :(得分:45)

执行此操作时:

int sum = IntStream.of(1, 2, 3, 4).map(i -> i + 1).sum();

每个链式方法都会在链中上一个方法的返回值上被调用

因此,map返回时调用IntStream.of(1, 2, 3, 4)sum返回时调用map(i -> i + 1)

您不必链接流方法,但是与使用以下等效代码相比,它更具可读性且不易出错:

IntStream is = IntStream.of(1, 2, 3, 4);
is = is.map(i -> i + 1);
int sum = is.sum();

与您在问题中显示的代码不同:

IntStream is = IntStream.of(1, 2, 3, 4);
is.map(i -> i + 1);
int sum = is.sum();

如您所见,您将忽略map返回的引用。这是错误的原因。


编辑(根据评论,感谢@IanKemp指出了这一点):实际上,这是错误的外部原因。如果您停止考虑,map必须对流本身做内部的操作,否则,终端操作将如何触发在每个流上传递给map的转换元件?我同意中间操作是惰性的,即当被调用时,它们对流的元素没有任何作用。但是在内部,它们必须在流管道本身中配置一些状态,以便以后可以应用。

尽管我不知道全部细节,但是发生的事情是,从概念上讲,map至少在做两件事:

  1. 它正在创建并返回一个新流,该流将作为参数传递的函数保存在某处,以便稍后在调用终端操作时可以将其应用于元素。

  2. 它还在旧流实例上设置了一个标志,即已对其进行调用的那个,指示该流实例不再代表管道的有效状态。这是因为保存传递给map的函数的新的更新状态现在已由返回的实例封装。 (我相信jdk团队可能已经做出了此决定,以使错误尽早出现,即,通过引发早期异常,而不是让管道继续处于不具有该功能的无效/旧状态,被应用,从而使终端操作返回意外结果。)

稍后,当在此实例上被标记为无效的实例上调用终端操作时,您将得到IllegalStateException。上面的两项构成了错误的深层内部原因。


查看所有这一切的另一种方法是通过中间操作或终端操作确保Stream实例仅操作一次。在这里您违反了此要求,因为您在同一实例上调用mapsum

实际上,javadocs for Stream明确指出:

  

一个流只能操作一次(调用中间流或终端流操作)。例如,这排除了“分叉”流,其中相同的源馈送了两个或多个管道,或同一流的多次遍历。如果流实现检测到正在重用该流,则可能抛出IllegalStateException。但是,由于某些流操作可能会返回其接收者而不是新的流对象,因此不可能在所有情况下都检测到重用。

答案 1 :(得分:15)

想象一下,IntStream是数据流的包装,带有一个 不变的操作列表。直到需要最终结果(本例中的总和)后,这些操作才会执行。 由于列表是不可变的,因此您需要一个新的IntStream实例,该实例的列表包含以前的项目以及新的项目,即'。地图的回报。

这意味着如果您不链接,您将在没有该操作的旧实例上进行操作。

流库还对正在发生的事情进行内部跟踪,这就是为什么它能够在sum步骤中引发异常。

如果您不想链接,则可以在每个步骤中使用一个变量:

IntStream is = IntStream.of(1, 2, 3, 4);
IntStream is2 = is.map(i -> i + 1);
int sum = is2.sum();

答案 2 :(得分:3)

  

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

取自https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html,位于“流操作和管道”下

  

在最低级别,所有流均由分隔器驱动。

来自“低级流构建”下的同一链接

  

横穿和分开排气元件;每个分割器仅对单个批量计算有用。

取自https://docs.oracle.com/javase/8/docs/api/java/util/Spliterator.html