收集器的组合器功能是否可以用于顺序流?

时间:2015-03-23 12:26:19

标签: java java-8 java-stream

示例程序:

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的组合器?

2 个答案:

答案 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只能通过并行执行来调用。