使用reduce和collect查找平均值

时间:2014-05-14 15:26:33

标签: java lambda functional-programming java-8 java-stream

我正在尝试了解新的Java 8 Stream API。

http://docs.oracle.com/javase/tutorial/collections/streams/reduction.html

我找到了使用collect API查找数字平均值的示例。但我觉得,同样可以使用reduce()来完成。

public class Test {

    public static void main(String[] args) {
        // Using collect
        System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
            .collect(Averager::new, Averager::accept, Averager::combine)
            .average());

        // Using reduce
        System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
            .reduce(new Averager(), (t, u) -> {
                t.accept(u);
                return t;
            }, (t, u) -> {
                t.combine(u);
                return t;
            }).average());
    }

    private static class Averager {
        private int total = 0;
        private int count = 0;

        public Averager() {
            // System.out.println("Creating averager");
        }

        public double average() {
            // System.out.println("Finding average");
            return count > 0 ? ((double) total) / count : 0;
        }

        public void accept(int i) {
            // System.out.println("Accepting " + i);
            total += i;
            count++;
        }

        public void combine(Averager other) {
            // System.out.println("Combining the averager : " + other);
            total += other.total;
            count += other.count;
        }

        @Override
        public String toString() {
            return "[total : " + total + ", count: " + count + "]";
        }
    }
}

1)有什么理由,我应该使用collect而不是reduce吗? 2)如果我启用所有调试系统,我可以看到执行的操作在收集和减少之间完全相同。在这两种情况下都没有使用合成器 3)如果我使流并行,收集总是返回正确的结果。 reduce()每次给我不同的结果 4)我不应该在并行流中使用reduce吗?

谢谢,
保罗

1 个答案:

答案 0 :(得分:18)

reducecollect之间的区别在于collect是一种增强的缩减形式,可以并行处理可变对象。 collect算法线程限制了各种结果对象,以便它们可以安全地进行变异,即使它们不是线程安全的。这就是Averager使用collect的原因。对于使用reduce的顺序计算,这通常不重要,但对于并行计算,它会给出不正确的结果,如您所观察到的那样。

关键点是reduce只要处理而不是可变对象就可以正常工作。你可以通过查看reduce的第一个参数来看到这一点。示例代码传递new Averager(),这是一个单个对象,它被并行缩减中的多个线程用作标识值。并行流的工作方式是将工作负载拆分为由各个线程处理的段。如果多个线程正在改变相同的(非线程安全的)对象,那么应该清楚为什么这会导致不正确的结果。

可以使用reduce来计算平均值,但是您需要使累积对象成为不可变的。考虑一个对象ImmutableAverager

static class ImmutableAverager {
    private final int total;
    private final int count;

    public ImmutableAverager() {
        this.total = 0;
        this.count = 0;
    }

    public ImmutableAverager(int total, int count) {
        this.total = total;
        this.count = count;
    }

    public double average() {
        return count > 0 ? ((double) total) / count : 0;
    }

    public ImmutableAverager accept(int i) {
        return new ImmutableAverager(total + i, count + 1);
    }

    public ImmutableAverager combine(ImmutableAverager other) {
        return new ImmutableAverager(total + other.total, count + other.count);
    }
}

请注意,我已调整acceptcombine的签名以返回新的ImmutableAverager,而不是改变this。 (这些更改也使方法与reduce的函数参数匹配,因此我们可以使用方法引用。)您可以像这样使用ImmutableAverager

    System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
            .parallel()
            .reduce(new ImmutableAverager(), 
                    ImmutableAverager::accept,
                    ImmutableAverager::combine)
            .average());

使用reduce的不可变值对象应该并行提供正确的结果。

最后,请注意IntStreamDoubleStreamsummaryStatistics()种方法,CollectorsaveragingDoubleaveragingIntaveragingLong可以为您完成这些计算的方法。但是,我认为问题更多的是关于收集和减少的机制,而不是关于如何最简洁地进行平均。