【问题标题】:Where is defined the combination order of the combiner of collect(supplier, accumulator, combiner)?collect(supplier, accumulator, combiner)的combiner的组合顺序在哪里定义?
【发布时间】:2015-05-29 09:44:43
【问题描述】:

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

用于组合两个值的关联、无干扰、无状态函数,必须与累加器函数兼容

combiner 是一个BiConsumer<R,R>,它接收两个R 类型的参数并返回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,我们必须以正确的顺序组合项目,否则在并行处理时可能会得到不一致的结果。

这在 Java 8 API 文档的任何部分中都有声明吗?还是真的没关系?

【问题讨论】:

标签: java java-8 java-stream


【解决方案1】:

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

如果您查看具有 BiConsumer&lt;R,? super T&gt; 类型的 accumulator 函数,就会有一个提示,换句话说,除了存储 T 类型的元素之外不能做任何其他事情,提供为第二个参数,放入 R 类型的容器中,作为第一个参数提供。

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

关联性约束表示拆分计算必须产生等效的结果。也就是说,对于任何输入元素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

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


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

但似乎这是一个较晚的更改,并且文档没有相应地进行调整。如果您查看包文档的Mutable reduction 部分,您会发现它已被改编为显示实际的Stream.collect 的签名和用法示例,但重复了与上面显示的关联性约束完全相同的定义,尽管finisher.apply(combiner.apply(a2, a3)) 在combiner 是BiConsumer 时不起作用...


文档问题已报告为 JDK-8164691 并在 Java 9 中得到解决。The new documentation 说:

combiner - 一个关联的、无干扰的、无状态的函数,它接受两个部分结果容器并将它们合并,它必须与累加器函数兼容。组合器函数必须将第二个结果容器中的元素折叠到第一个结果容器中。

【讨论】:

    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-30
      • 2012-04-30
      • 1970-01-01
      • 2010-11-29
      • 2017-07-11
      相关资源
      最近更新 更多