使用reduce和collect查找平均值

Pau*_*bin 5 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 + "]";
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

谢谢,
保罗

Stu*_*rks 19

reduce和之间的区别在于collectcollect是一种增强的缩减形式,可以并行处理可变对象.该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);
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,我已经调整了签名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());
Run Code Online (Sandbox Code Playgroud)

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

最后,注意IntStreamDoubleStreamsummaryStatistics()方法和Collectors拥有averagingDouble,averagingInt以及averagingLong能为你做这些计算方法.但是,我认为问题更多的是关于收集和减少的机制,而不是关于如何最简洁地进行平均.

  • 一个小的修正:收集实际上并不是减少的专业化,反之亦然.任何减少都可以表示为集合,而没有将集合表示为减少的一般方法(或者至少没有强制客户端代码来管理并发).所以实际上减少是一种特殊的收集形式. (2认同)