仔细阅读中的streams实现代码减少操作.java表明只有当ReduceTask
完成,并且ReduceTask
仅在并行评估管道时才使用实例。因此,在当前的实施中,在评估顺序管道时,永远不会调用组合器。
然而,规范中没有任何内容可以保证这一点。 ACollector
是一个对其实现提出要求的接口,并且对于顺序流没有任何豁免。就我个人而言,我发现很难想象为什么顺序管道评估可能需要调用组合器,但是比我更有想象力的人可能会找到它的巧妙用途,并实现它。规范允许这样做,即使今天的实现没有这样做,您仍然必须考虑它。
这应该不足为奇。 Streams API 的设计中心是支持与顺序执行同等的并行执行。当然,程序可以观察它是顺序执行还是并行执行。但 API 的设计是为了支持允许两者之一的编程风格。
如果您正在编写收集器,并且发现不可能(或不方便或困难)编写关联组合器函数,导致您想要将流限制为顺序执行,也许这意味着您正走向错误的方向。是时候退一步思考以不同的方式解决问题了。
不需要关联组合器函数的常见归约式操作称为向左折叠。主要特点是折叠功能严格从左到右应用,一次进行一个。我不知道并行左折叠的方法。
当人们试图以我们所说的方式扭曲收藏家时,他们通常会寻找类似左折叠的东西。 Streams API 没有对此操作的直接 API 支持,但编写起来非常容易。例如,假设您要使用此操作减少字符串列表:重复第一个字符串,然后附加第二个字符串。很容易证明这个操作不是关联的:
List<String> list = Arrays.asList("a", "b", "c", "d", "e");
System.out.println(list.stream()
.collect(StringBuilder::new,
(a, b) -> a.append(a.toString()).append(b),
(a, b) -> a.append(a.toString()).append(b))); // BROKEN -- NOT ASSOCIATIVE
按顺序运行,这会产生所需的输出:
aabaabcaabaabcdaabaabcaabaabcde
但是当并行运行时,它可能会产生如下结果:
aabaabccdde
由于它是按顺序“工作”的,因此我们可以通过调用来强制执行此操作sequential()
并通过让组合器抛出异常来支持这一点。此外,供应商必须只被调用一次。无法合并中间结果,因此如果供应商被调用两次,我们就已经遇到麻烦了。但由于我们“知道”供应商在顺序模式下仅被调用一次,因此大多数人并不担心这一点。事实上,我见过人们编写“供应商”来返回一些现有对象,而不是创建一个新对象,这违反了供应商合同。
在此使用 3-arg 形式collect()
,三个函数中有两个违反了合约。这难道不应该告诉我们以不同的方式做事吗?
这里的主要工作是由累加器函数完成的。为了完成折叠式缩减,我们可以使用严格的从左到右的顺序应用此函数forEachOrdered()
。我们必须在前后进行一些设置和完成代码,但这没有问题:
StringBuilder a = new StringBuilder();
list.parallelStream()
.forEachOrdered(b -> a.append(a.toString()).append(b));
System.out.println(a.toString());
当然,这在并行中工作得很好,尽管并行运行的性能优势可能会因以下的排序要求而被抵消:forEachOrdered()
.
总之,如果您发现自己想要进行可变归约,但缺少关联组合器函数,导致您将流限制为顺序执行,请将问题重新定义为向左折叠操作与使用forEachRemaining()
在你的累加器函数上。