哪里定义了收集器(供应商,累加器,合并器)的组合顺序?
Java API文档声明collect
方法的combiner
参数必须是:
用于组合两个值的关联,非干扰,无状态函数,它必须与累加器函数兼容
combiner
是BiConsumer
,它接收R
类型的两个参数并返回void
。 但是文档没有说明我们是否应该将元素组合到第一个或第二个参数中?
例如,以下示例可以给出不同的结果,这取决于组合的顺序为: m1.addAll(m2)
或m2.addAll(m1)
。
List res = LongStream .rangeClosed(1, 1_000_000) .parallel() .mapToObj(n -> "" + n) .collect(ArrayList::new, ArrayList::add,(m1, m2) -> m1.addAll(m2));
我知道在这种情况下我们可以简单地使用方法句柄,例如ArrayList::addAll
。 然而,在某些情况下需要一个Lambda,我们必须以正确的顺序组合项目,否则我们可能会在并行处理时得到不一致的结果。
这是否在Java 8 API文档的任何部分中声明了? 或者它真的没关系?
似乎文档中没有明确说明这一点。 然而,流API中有一个排序概念。 流可以是有序的也可以不是。 如果源spliterator是无序的(例如,如果流源是HashSet
),它可能从一开始就是无序的。 或者,如果用户明确使用了unordered()
操作,则流可能会变得无序。 如果流是有序的,那么收集过程也应该是稳定的,因此,我猜, 假设对于有序流, combiner
以严格的顺序接收参数。 然而,无法保证无序流。
当然,重要的是,当你使用m2.addAll(m1)
而不是m1.addAll(m2)
,它不仅改变了元素的顺序,而且完全打破了操作。 由于BiConsumer
不返回结果,因此您无法控制调用者将使用哪个对象作为结果,并且由于调用者将使用第一个,因此修改第二个将导致数据丢失。
如果你看一下具有BiConsumer
类型的累加器函数,有一个提示。 BiConsumer
,换句话说,除了将作为第二个参数提供的类型T
的元素存储到作为第一个参数提供的类型R
的容器中之外,不能做任何其他事情。
如果你看一下Collector
的文档 ,它使用BinaryOperator
作为组合函数,因此允许组合器决定返回哪个参数(甚至是一个完全不同的结果实例),你会发现:
关联约束表示拆分计算必须产生等效结果。 也就是说,对于任何输入元素
t1
和t2
,下面计算中的结果r1
和r2
必须是等价的: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
因此,如果我们假设累加器以相遇顺序应用,则组合器必须以从左到右的顺序组合第一个和第二个参数以产生等效结果。
现在, Stream.collect
的三个arg版本的签名略有不同,使用BiConsumer
作为BiConsumer
器, 完全支持方法引用,如ArrayList::addAll
。 假设所有这些操作的一致性并考虑到此签名更改的目的,我们可以安全地假设它必须是要修改的容器的第一个参数。
但似乎这是一个迟到的变化,文档没有相应调整。 如果你看一下包文档中的Mutable reduction部分,你会发现它已经过调整以显示实际的Stream.collect
的签名和用法示例,但重复与上面显示的关联约束完全相同的定义,尽管如果combiner
是BiConsumer
,则finisher.apply(combiner.apply(a2, a3))
不起作用的BiConsumer
……
文档问题已报告为JDK-8164691,并在Java 9中得到解决。 新文档说:
组合器 – 一种关联的,非干扰的无状态函数,它接受两个部分结果容器并合并它们,它们必须与累加器函数兼容。 组合器函数必须将元素从第二个结果容器折叠到第一个结果容器中。