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

为什么我必须在Java中链接流操作?,java,java-8,java-stream,Java,Java 8,Java Stream,我认为我所研究的所有资源都强调了流只能被消耗一次,而消耗是通过所谓的终端操作来完成的(我对此非常清楚) 出于好奇,我尝试了以下方法: 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); in

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

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

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
是一个中间(并且是惰性)操作,它本身对流不做任何操作。只有在调用终端操作sum(这是一个渴望的操作)时,流才会被消耗和操作

但我为什么要把它们锁起来

两者的区别是什么

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


想象IntStream是一个用 不可变的操作列表。直到您需要最终结果(在您的案例中求和),这些操作才会执行。 由于列表是不可变的,因此需要一个新的IntStream实例,该实例的列表包含以前的项和新项,这就是。地图的回报

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

流库还保持一些内部跟踪,这就是为什么它能够在
sum
步骤中抛出异常

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

IntStream is = IntStream.of(1, 2, 3, 4);
IntStream is2 = is.map(i -> i + 1);
int sum = is2.sum();
执行此操作时:

int sum = IntStream.of(1, 2, 3, 4).map(i -> i + 1).sum();
每个链式方法都在链中前一个方法的返回值上被调用

因此,在
IntStream.of(1,2,3,4)
返回的内容上调用
map
,在
map(i->i+1)
返回的内容上调用
sum

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

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
    。上面的两项配置了错误的深层内部原因


    查看所有这些的另一种方法是确保一个
    实例通过中间操作或终端操作只运行一次。这里您违反了这个要求,因为您在同一个实例上调用了
    map
    sum

    事实上,请明确说明:

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

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

    摘自“河流作业和管道”项下


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

    取自“低水位河流施工”下的同一链接

    横向和分离排气元件;每个拆分器仅用于单个批量计算


    取自

    一个流只能操作一次(调用中间或终端流操作)。例如,这排除了“分叉”流,即同一个源向两个或多个管道提供数据,或对同一个流进行多次遍历。如果流实现检测到流正在被重用,它可能会抛出IllegalStateException。但是,由于某些流操作可能返回其接收器而不是新的流对象,因此可能无法在所有情况下检测重用。所有流都由拆分器驱动,并且每个拆分器仅对单个批量有用computation@Koray若要添加,请添加
    IntStream is = IntStream.of(1, 2, 3, 4);
    is.map(i -> i + 1);
    int sum = is.sum();