示例程序:
public final class CollectorTest
{
private CollectorTest()
{
}
private static <T> BinaryOperator<T> nope()
{
return (t, u) -> { throw new UnsupportedOperationException("nope"); };
}
public static void main(final String... args)
{
final Collector<Integer, ?, List<Integer>> c
= Collector.of(ArrayList::new, List::add, nope());
IntStream.range(0, 10_000_000).boxed().collect(c);
}
}
因此,为了简化这里的问题,没有最终的转换,因此生成的代码非常简单。
现在,IntStream.range()
生成一个顺序流。我只是将结果打包成Integer
s,然后我设计的Collector
将它们收集到List<Integer>
中。很简单。
无论我运行这个示例程序多少次,UnsupportedOperationException
都不会命中,这意味着我的虚拟组合器永远不会被调用。
我有点期待这一点,但后来我已经误解了流,我不得不问这个问题......
当流保证顺序时,是否可以调用Collector
的组合器?
答案 0 :(得分:22)
仔细阅读ReduceOps.java中的流实现代码,发现只有在ReduceTask
完成时才会调用combine函数,并且仅在并行评估管道时才使用ReduceTask
实例。因此,在当前实现中 , 在评估顺序管道时从不调用组合器。
然而,规范中没有任何内容可以保证这一点。 Collector
是对其实现提出要求的接口,并且没有为顺序流授予豁免。就个人而言,我发现很难想象为什么顺序管道评估可能需要调用组合器,但是比我更有想象力的人可能会发现它的巧妙用途并实现它。规范允许它,即使今天的实现没有这样做,你仍然需要考虑它。
这应该不足为奇。流API的设计中心是通过顺序执行在平等的基础上支持并行执行。当然,程序可以观察它是顺序执行还是并行执行。但API的设计是支持一种允许任何一种编程风格。
如果你正在编写一个收藏家,你发现写一个关联组合函数是不可能的(或者不方便或者很难),导致你想要将你的流限制为顺序执行,也许这个意味着你正朝着错误的方向前进。现在是时候退后一步,考虑以不同的方式解决问题。
不需要关联组合器功能的常见缩减式操作称为 fold-left 。主要特点是折叠功能严格从左到右应用,一次进行一次。我不知道如何并行化左折叠。
当人们试图以我们一直在讨论的方式扭曲收藏家时,他们通常会寻找像左撇子这样的东西。 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
因为它&#34;工作&#34;顺便说一下,我们可以通过调用sequential()
来强制执行此操作,并通过让组合器抛出异常来支持它。此外,供应商必须只调用一次。没有办法合并中间结果,所以如果供应商被叫两次,我们就已经遇到了麻烦。但是因为我们知道&#34;供应商只能按顺序模式调用一次,大多数人都不担心这一点。事实上,我已经看到人们写了#34;供应商&#34;违反供应商合同,返回一些现有对象而不是创建新对象。
在{-1}}的3-arg形式的使用中,我们有三个函数中的两个破坏了他们的合同。难道这不是告诉我们以不同的方式做事吗?
这里的主要工作是由累加器功能完成的。为了实现折叠式缩减,我们可以使用collect()
以严格的从左到右的顺序应用此功能。我们必须在之前和之后做一些设置和完成代码,但这没问题:
forEachOrdered()
当然,这可以并行工作,但并行运行的性能优势可能会因StringBuilder a = new StringBuilder();
list.parallelStream()
.forEachOrdered(b -> a.append(a.toString()).append(b));
System.out.println(a.toString());
的排序要求而有所否定。
总之,如果您发现自己想要进行可变缩减,但是您缺少关联组合器功能,导致您将流限制为顺序执行,则将问题重新设置为 fold-left < / em>操作并在累加器函数上使用forEachOrdered()
。
答案 1 :(得分:2)
正如之前@MarkoTopolnik和@Duncan的评论中所观察到的,无法保证在顺序模式下调用Collector.combiner()
来产生减少的结果。事实上,Java doc在这一点上有点主观,这可能导致不恰当的解释。
(...)并行实现将对输入进行分区,为每个分区创建结果容器,将每个分区的内容累积到该分区的子结果中,然后然后使用组合器函数合并子结果合并后的结果。
根据NoBlogDefFound组合器仅用于并行模式。请参阅以下部分报价:
combiner()用于将两个累加器连接成一个。当收集器并行执行时,使用它,首先独立地拆分输入流和收集部件。
为了更清楚地说明这个问题,我重新编写了第一个代码,并提出了两种方法(串行和并行)。
public final class CollectorTest
{
private CollectorTest()
{
}
private static <T> BinaryOperator<T> nope()
{
return (t, u) -> { throw new UnsupportedOperationException("nope"); };
}
public static void main(final String... args)
{
final Collector<Integer, ?, List<Integer>> c =
Collector
.of(ArrayList::new, List::add, nope());
// approach sequential
Stream<Integer> sequential = IntStream
.range(0, 10_000_000)
.boxed();
System.out.println("isParallel:" + sequential.isParallel());
sequential
.collect(c);
// approach parallel
Stream<Integer> parallel = IntStream
.range(0, 10_000_000)
.parallel()
.boxed();
System.out.println("isParallel:" + parallel.isParallel());
parallel
.collect(c);
}
}
运行此代码后,我们可以获得输出:
isParallel:false
isParallel:true
Exception in thread "main" java.lang.UnsupportedOperationException: nope
at com.stackoverflow.lambda.CollectorTest.lambda$nope$0(CollectorTest.java:18)
at com.stackoverflow.lambda.CollectorTest$$Lambda$3/2001049719.apply(Unknown Source)
at java.util.stream.ReduceOps$3ReducingSink.combine(ReduceOps.java:174)
at java.util.stream.ReduceOps$3ReducingSink.combine(ReduceOps.java:160)
因此,根据这个结果,我们可以推断Collector's combiner
只能通过并行执行来调用。