`Stream`的`Collectors.reducing`与`Collectors.groupingBy`

前言

Java8提供的Stream接口使流式编程和函数式编程更加容易。

现在一些集合的处理,经常会使用Stream来进行处理,相比循环,代码的可读性有所提高。如果更进一步,再利用上Reactor进行反应式编程,则会带来更多优势,如异常处理、执行线程控制、并行、缓冲等,声明式的完成了许多命令式编程许多代码才能完成的功能。

场景

一次使用Stream进行收集的过程中,同时使用Collectors.groupingByCollectors.reducing,出现了问题。这里记录一下。

场景是一批业务对象Foo

@Getter
@Setter
@ToString
class Foo{
    // 分组属性
    private String name;
    // 需求:分组平均值
    private Integer m;
    // 需求:分组平均值
    private Integer n;
    // 非业务属性,计算平均值时使用
    private Integer count;

    public Foo(String name, Integer m, Integer n) {
        this.name = name;
        this.m = m;
        this.n = n;
    }
}
复制代码

笔者想根据Stream<Foo>对这个流先按照name分组收集,再每组统计m,n属性的平均值,最终仍返回一个Foo,便类似下面这样写了出来:

Foo f1 = new Foo("l", 1, 2),
        f2 = new Foo("l", 1, 2),
        f3 = new Foo("l", 1, 2),
        f4 = new Foo("c", 1, 2),
        f5 = new Foo("c", 1, 2),
        f6 = new Foo("q", 1, 2),
        f7 = new Foo("q", 1, 2);
System.out.println(Stream.of(f1, f2, f3, f4, f5, f6, f7)
        .collect(Collectors.groupingBy(
                Foo::getName,
                Collectors.reducing(new Foo(null, 0, 0), (o, p) -> {
                    if (o.getName() == null) {
                        o.setName(p.getName());
                    }
                    o.setM(o.getM() + p.getM());
                    o.setN(o.getN() + p.getN());
                    o.setCount(o.getCount() == null ? 1 : o.getCount() + 1);
                    return o;
                })
        ))
);
复制代码

最后发现结果为:

{q=Foo(name=l, m=7, n=14, count=6), c=Foo(name=l, m=7, n=14, count=6), l=Foo(name=l, m=7, n=14, count=6)}
复制代码

按照名称分组后各组的Foo都是一个,代码并未向预想那样,先groupingBy分组,再按照每组进行reducing收集,而是把原始Stream全收集为一个对象给了每组作为值。

原因和解决方案

查了查StackOverflow,看了下Collectors.reducing的源码:

public static <T> Collector<T, ?, T> reducing(T identity, BinaryOperator<T> op) {
    return new CollectorImpl<>(
	        // 作为accumulator的函数式接口,看下段
            boxSupplier(identity),
            (a, t) -> { a[0] = op.apply(a[0], t); },
            (a, b) -> { a[0] = op.apply(a[0], b[0]); return a; },
            a -> a[0],
            CH_NOID);
}

// 重点,每次只返回传进来的对象,而不是新创建一个
private static <T> Supplier<T[]> boxSupplier(T identity) {
    return () -> (T[]) new Object[] { identity };
}
复制代码

发现了,Collectors.reducing 所创建的 accumulator 返回的是一个传进来的固定对象,不会因为上游是Collectors.groupingBy就新创建然后分组,所以最好不要修改Collectors.reducing的参数

最新的需求可以这样解决:

System.out.println(Stream.of(f1, f2, f3, f4, f5, f6, f7)
        .collect(Collectors.groupingBy(
                Foo::getName,
	            // 新构建Collector,每组accumulator会新创建Foo
                Collector.of(() -> new Foo(null, 0, 0), (o, p) -> {
                    if (o.getName() == null) {
                        o.setName(p.getName());
                    }
                    o.setM(o.getM() + p.getM());
                    o.setN(o.getN() + p.getN());
                    o.setCount(o.getCount() == null ? 1 : o.getCount() + 1);
                }, (o, p) -> o)
        ))
);
复制代码

输出了想要的结果:

{q=Foo(name=q, m=2, n=4, count=2), c=Foo(name=c, m=2, n=4, count=2), l=Foo(name=l, m=3, n=6, count=3)}
复制代码

参考

stackoverflow.com/questions/4…

JDK11.0.2

猜你喜欢

转载自juejin.im/post/5eca1862f265da76e46e471a