为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

扬帆大鱼 2022-01-12 17:14:48
我认为我研究过的所有资源都以一种或另一种方式强调一个流只能被消费一次,并且消费是通过所谓的终端操作完成的(这对我来说很清楚)。只是出于好奇,我尝试了这个: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)这很常见,我遗漏了一些东西,但仍然想问:据我所知,这map是一个中间(和惰性)操作,它本身对 Stream没有任何作用。只有当终端操作sum(即急切操作)被调用时,Stream 才会被消费和操作。但为什么我必须把它们锁起来?和有什么区别is.map(i -> i + 1);is.sum();和is.map(i -> i + 1).sum();?
查看完整描述

3 回答

?
手掌心

TA贡献1942条经验 获得超3个赞

当你这样做时:


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它至少做了两件事:


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


它还为旧的流实例设置一个标志,即它已被调用的那个流实例,表明该流实例不再代表管道的有效状态。这是因为保存传递给的函数的新的更新状态map现在被它返回的实例封装。(我相信这个决定可能是 jdk 团队做出的让错误尽早出现的决定,即通过抛出一个早期异常而不是让管道以一个无效/旧状态继续,该状态不包含函数被应用,从而让终端操作返回意外的结果)。


稍后,当在这个标记为无效的实例上调用终端操作时,你会得到IllegalStateException. 以上两项配置了错误的深层内部原因。


另一种查看这一切的方法是确保Stream实例仅被操作一次,通过中间操作或终端操作。在这里你违反了这个要求,因为你在同一个实例上调用map和sum。


事实上,javadocsStream清楚地说明了这一点:


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


查看完整回答
反对 回复 2022-01-12
?
白衣非少年

TA贡献1155条经验 获得超0个赞

想象一下 IntStream 是您的数据流的包装器,具有不可变的操作列表。在您需要最终结果(在您的情况下为总和)之前,不会执行这些操作。由于列表是不可变的,因此您需要一个新的 IntStream 实例,其中包含一个包含先前项目和新项目的列表,即 '. 地图'返回。


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


流库还对正在发生的事情进行一些内部跟踪,这就是它能够在sum步骤中抛出异常的原因。


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


IntStream is = IntStream.of(1, 2, 3, 4);

IntStream is2 = is.map(i -> i + 1);

int sum = is2.sum();


查看完整回答
反对 回复 2022-01-12
?
哆啦的时光机

TA贡献1779条经验 获得超6个赞

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

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

在最低级别,所有流都由拆分器驱动。

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

遍历和拆分排气元件;每个 Spliterator 仅对单个批量计算有用。

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


查看完整回答
反对 回复 2022-01-12
  • 3 回答
  • 0 关注
  • 287 浏览

添加回答

举报

0/150
提交
取消
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号