collect(supplier, accumulator, combiner)的组合器的组合顺序在哪里定义?

Where is defined the combination order of the combiner of collect(supplier, accumulator, combiner)?

Java API 文档指出 collect 方法的 combiner 参数必须是:

an associative, non-interfering, stateless function for combining two values, which must be compatible with the accumulator function

A combiner 是一个 BiConsumer<R,R>,它接收两个类型为 R 和 returns void 的参数。但是文档没有说明我们是否应该将元素组合到第一个或第二个参数中?

例如,以下示例可能会给出不同的结果,具体取决于组合顺序:m1.addAll(m2)m2.addAll(m1)

List<String> res = LongStream
     .rangeClosed(1, 1_000_000)
     .parallel()
     .mapToObj(n -> "" + n)
     .collect(ArrayList::new, ArrayList::add,(m1, m2) -> m1.addAll(m2));

我知道在这种情况下我们可以简单地使用方法句柄,例如 ArrayList::addAll。然而,在某些情况下需要 Lambda,我们必须以正确的顺序组合这些项目,否则在并行处理时我们可能会得到不一致的结果。

Java8API 文档的任何部分是否声明了这一点?还是真的不重要?

文档中似乎没有明确说明。但是在流 API 中有一个 ordering 概念。可以订购或不订购流。如果 source spliterator 是无序的(例如,如果流源是 HashSet),它可能从一开始就是无序的。或者,如果用户明确使用 unordered() 操作,流可能会变得无序。如果流是有序的,那么收集过程也应该是稳定的,因此,我猜,假设 对于有序流,combiner 以严格的顺序接收参数。但是,不能保证无序流。

当然,这很重要,因为当您使用 m2.addAll(m1) 而不是 m1.addAll(m2) 时,它不仅改变了元素的顺序,而且完全破坏了操作。由于 BiConsumer 不是 return 结果,因此您无法控制调用者将使用哪个对象作为结果,并且由于调用者将使用第一个对象,因此修改第二个对象将导致数据丢失.

如果你查看类型为 BiConsumer<R,? super T>accumulator 函数,会有一个提示,换句话说,除了存储元素之外不能做任何其他事情将类型 T 作为第二个参数提供到类型 R 的容器中,作为第一个参数提供。

如果您查看 documentation of Collector,它使用 BinaryOperator 作为 combiner 函数,因此允许 combiner 来决定 return 的哪个参数(或者甚至是一个完全不同的结果实例),你会发现:

The associativity constraint says that splitting the computation must produce an equivalent result. That is, for any input elements t1 and t2, the results r1 and r2 in the computation below must be equivalent:

A a1 = supplier.get();
accumulator.accept(a1, t1);
accumulator.accept(a1, t2);
R r1 = finisher.apply(a1);  // result without splitting

A a2 = supplier.get();
accumulator.accept(a2, t1);
A a3 = supplier.get();
accumulator.accept(a3, t2);
R r2 = finisher.apply(combiner.apply(a2, a3));  // result with splitting

因此,如果我们假设 accumulator 以遇到顺序应用,则 combiner 必须组合左侧的第一个和第二个参数-to-right 顺序以产生等效结果。


现在,Stream.collect 的三参数版本的签名略有不同,使用 BiConsumer 作为 combiner 。假设所有这些操作的一致性并考虑此签名更改的目的,我们可以安全地假设它必须是要修改的容器的第一个参数。

但似乎这是一个较晚的更改,文档没有相应地调整。如果您查看包文档的 Mutable reduction 部分,您会发现它已被改编以显示实际的 Stream.collect 的签名和用法示例,但重复了关于关联性约束的完全相同的定义如上所示,尽管如果 combinerBiConsumer

,则 finisher.apply(combiner.apply(a2, a3)) 不起作用

文档问题已报告为 JDK-8164691 and addressed in Java 9. The new documentation 说:

combiner - an associative, non-interfering, stateless function that accepts two partial result containers and merges them, which must be compatible with the accumulator function. The combiner function must fold the elements from the second result container into the first result container.