Java Stream--(4)定制收集器

130 阅读3分钟

本文中我们讨论一下如何使用java8中Stream API来为自己定制收集器(Collector)。

收集器接口介绍

先看一下Collector接口的定义,这里只列出了接口的抽象方法。

package java.util.stream;

public interface Collector<T,A,R> {
    Supplier<A> supplier();
    BiConsumer<A,T> accumulator();
    BinaryOperator<A> combiner();
    Function<A,R> finisher();
    Set<Characteristics> characteristics();
}

在开始讲解每个抽象方法之前,我们看一下Stream::collect的一个简化版实现:

private static <T,A,R> R collect(
        Collector<T,A,R> collector, Iterable<T> elements) {

    Supplier<A> supplier = collector.supplier();
    A acc = supplier.get();

    BiConsumer<A,T> accumulator = collector.accumulator();
    for (T elem: elements) {
        accumulator.accept(acc, elem);
    }

    Function<A,R> finisher = collector.finisher();
    R result = finisher.apply(acc);

    return result;
}

现在我们更详细的说一下Collector接口。 首先从Collector<T,A,R>声明的泛型参数开始:

  • T -- Stream的元素类型
  • A -- 用来保持收集操作中间结果的辅助操作类型,一般被称作累加器。
  • R -- 收集器操作的结果类型

现在说一下Collector接口的5个抽象方法:

  • supplier() -- 返回一个Supplier<A>实例,用于创建累加器对象。
  • accumulator() -- 返回一个BiConsumer<A,T>实例,用来描述流中的当前元素怎么和累加器产生作用。以sum()这个Collector为例,BiConsumer负责把每个元素的值累加到累加器上。
  • finisher() -- 返回一个函数Function<A,R>,把累加器对象转化为最终结果。
  • combiner() -- 并行流处理时,把各个并行计算的分结果合并成一个单一结果。
  • characteristics() -- 这个方法返回一个描述当前收集器特性的集合。返回一个空集合是一个比较安全的缺省值。

关于收集器的特性,有如下3个值中的0个到3个的集合:

  • Characteristics.CONCURRENT -- 表示结果容器支持多个线程在结果容器上并发的调用累加器函数
  • Characteristics.UNORDERED -- 表示集合操作无效保留元素的出现顺序
  • Characteristics.IDENTITY_FINISH -- 表示终止器函数返回其参数而不做任何修改

收集器例1-实现取得列表最大值最小值

我们创建一个简单的收集器,用来取得流中最大值元素和最小值元素。

public class MinMaxCollector<T>
    implements Collector<T,
    MinMaxCollector.MinMaxAccumulator<T>,
    MinMaxCollector.MinMax<T>> {

    // 结果类型
    public static class MinMax<T> {
        private final Optional<T> min;
        private final Optional<T> max;

        public MinMax(T min, T max) {
            this.min = Optional.ofNullable(min);
            this.max = Optional.ofNullable(max);
        }

        public Optional<T> getMin() {
            return min;
        }

        public Optional<T> getMax() {
            return max;
        }
    }

    // 累加器对象
    public static class MinMaxAccumulator<T> {
        private final Comparator<? super T> cmp;

        private T min = null;
        private T max = null;

        public MinMaxAccumulator(Comparator<? super T> cmp) {
            this.cmp = cmp;
        }

        public void accumulate(T elem) {
            min = (min == null || cmp.compare(elem, min) < 0)
                ? elem : min;

            max = (max == null || cmp.compare(elem, max) > 0)
                ? elem : max;
        }

        public MinMaxAccumulator<T> combine(MinMaxAccumulator<T> other) {
            MinMax<T> otherMinMax = other.toMinMax();
            otherMinMax.getMax().ifPresent(this::accumulate);
            otherMinMax.getMin().ifPresent(this::accumulate);
            return this;
        }

        public MinMax<T> toMinMax() {
            return new MinMax<>(min, max);
        }
    }

    private final Comparator<? super T> cmp;

    public MinMaxCollector(Comparator<? super T> cmp) {
        this.cmp = Objects.requireNonNull(cmp);
    }

    @Override
    public Supplier<MinMaxAccumulator<T>> supplier() {
        return () -> new MinMaxAccumulator<>(cmp);
    }

    @Override
    public BiConsumer<MinMaxAccumulator<T>, T> accumulator() {
        return MinMaxAccumulator::accumulate;
    }

    @Override
    public BinaryOperator<MinMaxAccumulator<T>> combiner() {
        return MinMaxAccumulator::combine;
    }

    @Override
    public Function<MinMaxAccumulator<T>, MinMax<T>> finisher() {
        return MinMaxAccumulator::toMinMax;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

测试我们的收集器

员工类Emp包含员工姓名、年龄和工资三个字段,我们创建一些员工,分别按照工资和年龄使用我们创建的收集器取得最大最下年龄员工和最高最低薪水员工。

@Test
public void test1() {
    List<Emp> emps = Stream.of(
        new Emp("a", 20, 6000),
        new Emp("b", 30, 5000),
        new Emp("c", 40, 4000),
        new Emp("d", 50, 3000),
        new Emp("e", 60, 2000)
    ).collect(Collectors.toList());

    MinMaxCollector.MinMax<Emp> ageMinMax = emps.stream()
        .collect(new MinMaxCollector<>(comparing(Emp::getAge)));

    System.out.println("by age:" + JSON.toJSONString(ageMinMax));

    MinMaxCollector.MinMax<Emp> salaryMinMax = emps.stream()
        .collect(new MinMaxCollector<>(comparing(Emp::getSalary)));

    System.out.println("by salary:" + JSON.toJSONString(salaryMinMax));
}

输出结果如下:

by age:{"max":{"age":60,"name":"e","salary":2000},"min":{"age":20,"name":"a","salary":6000}}
by salary:{"max":{"age":20,"name":"a","salary":6000},"min":{"age":60,"name":"e","salary":2000}}

可见取得了我们期望的结果。