Java 中间操作什么时候命中每个元素?

When does a Java intermediate operation hit every element?

我对 Java 流的工作方式感到困惑,尤其是在短路方面。作为一个让我感到困惑的例子,我编写了以下示例:

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Optional<Integer> res = list.stream()
        .map(x -> {
            System.out.println("first map: " + x);
            return 2*x;
        })
        .sorted((a,b)-> {
            System.out.println("sorting " + a + " : " + b);
            return a - b;
        })
        .map(x -> {
            System.out.println("second map: " +  x);
            return 2*x;
        })
        .findAny();
System.out.println("found " + res.get());

有输出

first map: 1
first map: 2
first map: 3
first map: 4
first map: 5
first map: 6
first map: 7
first map: 8
first map: 9
first map: 10
sorting 4 : 2
sorting 6 : 4
sorting 8 : 6
sorting 10 : 8
sorting 12 : 10
sorting 14 : 12
sorting 16 : 14
sorting 18 : 16
sorting 20 : 18
second map: 2
found 4

执行时,这段代码演示了在中间调用 sorted 会强制第一个映射应用于流中的每个元素。但是,第二个映射并未应用于流中的每个元素,因为 findAny 将其短路。

基本上我的问题是:这里的规则是什么?为什么 Java 足够聪明,知道它不需要在流的每个元素上调用第二个映射,但又不够聪明,不知道最后的 findAny 不需要它实际排序任何东西。

我试过“阅读手册”,但我不太清楚。

发生这种情况是因为流一次懒惰地处理来自源的一个元素。

每个操作只在需要时发生。

在排序的情况下,流会将所有数据从源转储到内存,因为无法对元素逐个排序。

map --> sorted --> map --> findAny

第一次 map 操作应用于所有元素,因为它先于 sorting 操作。

完成排序后,将一次处理一个元素。

终端操作findAny是一个短路操作。因此,终端操作只会查看所有元素中的第一个,第二个 map 操作只需要应用一次。


Why is Java smart enough to know it doesn't need to call the second map on every element of the stream but not smart enough to know that findAny at the end doesn't require it to actually sort anything

嗯,我猜是因为直到现在 findAny() 在单线程执行的情况下仍然表现得与 findFirst() 完全一样,这将需要进行排序才能找到正确的结果。

您可以尝试使用顺序流并发现 findAny()findFirst() 产生相同的结果。

我不能告诉你为什么它会这样。

Java-doc 谨慎地说 findAny()

The behavior of this operation is explicitly nondeterministic

仅当流以并行方式处理时 findAny() 不同于 findFirst().

findAny() 可以 return 一个被任何线程偷看的元素,而 findFirst() 保证尊重提供结果时的初始顺序。


https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/Stream.html

这确实不明显,因为实际原因是历史性的。 Java 8 的第一个发布版本有一个 Stream 实现,它确实考虑了 findAny 的无序性质,至少在某些情况下是这样。

不是可跳过的 sorted 但在更多情况下是正确的。这已在

中讨论过

在后面的Q&A中我们发现,其中一位架构师:

After some analysis we chose to back out back-propagation entirely. The only place where this pays off is optimizing away a sort; if you have a pipeline that sorts into an unordered terminal op, that's probably a user error anyway.

所以参考实现的当前状态是终端操作的无序性质没有用于优化前面的中间操作。

这并不意味着这样的优化是无效的;当后续操作不关心顺序时跳过 sort 的示例被明确提及为有效,尽管 Brian Goetz 认为这种情况无论如何都可能是开发人员方面的错误。

我的观点略有不同,因为我认为如果你有 Stream returning 方法,这将是一个有用的场景,它可能 return 一个排序的 Stream,而操作由调用者确定此排序是否与最终结果相关。

但是有没有这样的优化并不影响结果的正确性。请注意,这不仅适用于无序操作。

当你有

Optional<Integer> res = IntStream.rangeClosed(1, 10)
    .mapToObj(x -> {
        System.out.println("first map: " + x);
        return 2*x;
    })
    .sorted((a,b)-> {
        System.out.println("sorting " + a + " : " + b);
        return Integer.compare(a, b);
    })
    .map(x -> {
        System.out.println("second map: " +  x);
        return 2*x;
    })
    .findFirst();

将操作转换为(等价于)

将是完全有效的
Optional<Integer> res = IntStream.rangeClosed(1, 10)
    .mapToObj(x -> {
        System.out.println("first map: " + x);
        return 2*x;
    })
    .min((a,b)-> {
        System.out.println("sorting " + a + " : " + b);
        return Integer.compare(a, b);
    })
    .map(x -> {
        System.out.println("second map: " +  x);
        return 2*x;
    });

虽然第一个映射函数和比较器的求值顺序不同,但是return终端操作的正确结果会更高效return。

但正确的结果才是最重要的。你根本不应该假设一个特定的评估顺序。你在手册中找不到评估顺序的规范,因为它被故意遗漏了。优化的存在与否可以随时改变。如果您想要一个生动的示例,请将 findAny() 替换为 count() 并比较示例在 Java 8 和 Java 9(或更新版本)中的行为。

// run with Java 8 and Java 9+ and compare...
long count = IntStream.rangeClosed(1, 10)
    .mapToObj(x -> {
        System.out.println("first map: " + x);
        return 2*x;
    })
    .sorted((a,b)-> {
        System.out.println("sorting " + a + " : " + b);
        return Integer.compare(a, b);
    })
    .map(x -> {
        System.out.println("second map: " +  x);
        return 2*x;
    })
    .count();
System.out.println("found " + count + " elements");