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

2022-09-01 19:55:49

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

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

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没有任何作用。仅当调用终端操作(这是预先操作)时,流才会被消耗操作mapsum

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

两者之间有什么区别

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

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

?


答案 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


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

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

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

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

稍后,当在此标记为无效的实例上调用终端操作时,您将获得 .上面的两个项目配置了错误的深层内部原因。IllegalStateException


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

事实上,Javadocs for Stream清楚地表明了这一点:

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


答案 2

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

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

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

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

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

推荐