【问题标题】:Why do I have to chain Stream operations in Java? [duplicate]为什么我必须在 Java 中链接 Stream 操作? [复制]
【发布时间】:2018-11-12 02:04:44
【问题描述】:

我认为我研究过的所有资源都以一种或另一种方式强调一个流只能被消费一次,并且消费是通过所谓的终端操作来完成的(这对我来说很清楚)。

出于好奇,我尝试了这个:

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 执行 nothing。只有当终端操作sum(即急切操作)被调用时,Stream才会消费操作

但是为什么我必须把它们锁起来呢?

有什么区别

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

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

?

【问题讨论】:

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

标签: java java-8 java-stream


【解决方案1】:

当你这样做时:

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

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

因此,mapIntStream.of(1, 2, 3, 4) 返回的内容上被调用,summap(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 返回的引用。这是错误的原因。


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

尽管我不了解全部细节,但从概念上讲,map 至少做了两件事:

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

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

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


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

其实javadocs for Stream说的很清楚:

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

【讨论】:

  • @Koray 要补充一点,在你的两条线 is.map(i -> i + 1); int sum = is.sum(); 添加一个调试点,你会注意到 IntPipeline is 的变化,这是你没有重用的,我相信因为@在您的情况下,987654342@ 已标记为 linkedOrConsumed,您会看到一个 ISE。
  • @nullpointer 我不确定。 Memorization 听起来与 memoization 太相似了,这是完全不同的东西......也许 tracking 是一个更好的术语?
  • 这感觉非常类似于 Java 中古老的 str.toLowerCase();s = str.toLowerCase(); 的绊脚石
  • “如您所见,您忽略了 map 返回的引用。这是错误的原因。” - 这对我来说没有意义。正如提问者所指出的,map() 是一个中间操作,这意味着应用它然后通过is.map(i -> i + 1); 丢弃结果应该对底层流没有影响,从而调用终端对该底层流的操作int sum = is.sum(); 仍应成功。
  • 我同意@IanKemp 的观点,即这个答案并没有真正回答这个问题。当中间操作返回一个新流时,人们会认为旧流保持不变。该界面暗示了不变性。
【解决方案2】:

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

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

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

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

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

【讨论】:

    【解决方案3】:

    中间操作返回一个新流。他们总是很懒惰;执行诸如 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

    【讨论】:

    • 是的......但这并不能解释如果你忽略结果会发生什么。
    • 你是对的,我更新了我的答案
    猜你喜欢
    • 2011-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-05
    • 2020-02-26
    • 1970-01-01
    相关资源
    最近更新 更多