为什么我必须用Java链接Stream操作? [重复]

问题描述 投票:23回答:3

这个问题在这里已有答案:

我认为我研究过的所有资源都强调一个流只能被消耗一次,消费是通过所谓的终端操作完成的(这对我来说非常清楚)。

出于好奇我试过这个:

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(这是一个急切的操作)时,才会消耗和操作流。

但为什么我要把它们连在一起呢?

有什么区别

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

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

?

java java-8 java-stream
3个回答
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。但是,由于某些流操作可能返回其接收器而不是新的流对象,因此可能无法在所有情况下检测重用。


15
投票

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

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

流库还保留了对正在发生的事情的内部跟踪,这就是为什么它能够在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的“Stream Operations and Pipelines”

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

取自“低级别流建设”下的相同链接

横向和分离排气元件;每个Spliterator仅对单个批量计算有用。

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

© www.soinside.com 2019 - 2024. All rights reserved.