Java8中重要的收集器Collector

Posted 玉龙小主

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8中重要的收集器Collector相关的知识,希望对你有一定的参考价值。

Collector介绍

Java8的stream api能很方便我们对数据进行统计分类等工作,函数式编程的风格让我们方便并且直观地编写统计代码。

例如:

Stream<Integer> stream = Stream.iterate(1, item -> item+2).limit(6);
//        stream.filter(item -> item > 2).mapToInt(item -> item * 2).skip(2).limit(2).sum();//.reduce(0, (val, val2)->val+val2);
//        System.out.println(sum);
IntSummaryStatistics summaryStatistics = stream.filter(item -> item > 2).mapToInt(item -> item * 2).skip(2).summaryStatistics();
System.out.println(summaryStatistics.getAverage());

stream里有一个collect(Collector c)方法,这个方法里面接收一个Collector的实例。这里我们要弄清楚Collector与Collectors之间的关系。

作为collect方法的参数,Collector是一个接口,它是一个可变的汇聚操作,将输入元素累计到一个可变的结果容器中;它会在所有元素都处理完毕后,将累积的结果转换为一个最终的表示(这是一个可选操作);

这些如果你不太懂,请继续往下看,结合下面自定义Collector,相信你可以理解这些内容。

Collectors本身提供了关于Collector的常见汇聚实现,Collectors的内部类CollectorImpl实现了Collector接口,Collectors本身实际上是一个工厂。

Collector的使用

很多时候我们会用到Collectors的toList方法,Collectors中提供了将流中元素累积到汇聚结果的各种方式,例如Counting、Joining、maxBy等等。下面是一个例子:

//分组
        List<Student> list1 = Arrays.asList(s1, s2, s3, s4, s5);
        //根据名称分组
//        Map<String, List<Student>> map = list1.stream().collect(Collectors.groupingBy(Student::getName));
        //先根据名称分组,然后求每组平均分
        Map<String, Double> map  = list1.stream().collect(Collectors.groupingBy(Student::getName, Collectors.averagingDouble(Student::getScore)));
        System.out.println(map);

        //分区
        Map<Boolean, List<Student>> map1 = list1.stream().collect(Collectors.partitioningBy(item -> item.getScore() >= 90));
        System.out.println(map1);

        System.out.println("------2-----");

        //先根据名称分组再根据分数分组
        Map<String, Map<Integer, List<Student>>> map2 = list1.stream().collect(Collectors.groupingBy(Student::getName, Collectors.groupingBy(Student::getScore)));
        System.out.println(map2);

这里分区分组与数据库中的分区分组的概念类似。

Collectors are designed to be composed; many of the methods in {@link Collectors} are functions that take a collector and produce a new collector.

附上javadoc上的一句话,这句话说明收集操作是可以嵌套的。

自定义Collector

前面讲过,Collectors本身提供了关于Collector的常见汇聚实现,那么程序员自身也可以根据情况定义自己的汇聚实现。

首先我们看下Collector接口的结构

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

    Set<Characteristics> characteristics();
}

其中这里的泛型所表示的含义是:
T:表示流中每个元素的类型。
A:表示中间结果容器的类型。
R:表示最终返回的结果类型。

Collector中还定义了一个枚举类Characteristics,有三个枚举值,理解这三个值的含义对于我们自己编写正确的收集器也是至关重要的。

  • Characteristics.CONCURRENT:表示中间结果只有一个,即使在并行流的情况下。所以只有在并行流且收集器不具备CONCURRENT特性时,combiner方法返回的lambda表达式才会执行(中间结果容器只有一个就无需合并)。
  • Characteristics.UNORDER:表示流中的元素无序。
  • Characteristics.IDENTITY_FINISH:表示中间结果容器类型与最终结果类型一致,此时finiser方法不会被调用。

我们再来看一下Collectors中toList方法的实现。

public static <T> Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   CH_ID);
    }

static final Set<Collector.Characteristics> CH_ID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));

 

到此,不知你是否对自定义收集器有那么点感觉了?

那么到底怎么实现自定义收集器呢,下面举例子来看看。

public class MyCollectorImpl<T> implements Collector<T, Set<T>, Set<T>> {
    @Override
    public Supplier<Set<T>> supplier() {
        return HashSet<T>::new;
    }

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

    @Override
    public BinaryOperator<Set<T>> combiner() {
        return (set, item) -> {set.addAll(item); return set;};
    }

    @Override
    public Function<Set<T>, Set<T>> finisher() {

        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH,UNORDERED));
    }

    public static void main(String[] args) {
        List<String> list = Arrays.asList("hello", "world", "welcome");
        Set<String> set = list.stream().collect(new MyCollectorImpl<>());
        set.forEach(System.out::println);
    }
}

这是一个简单的例子,我们给收集器加上了IDENTITY_FINISH特性,此时finisher方法返回的lambda表达式是不会得到调用的。这一点也可以从源码中得到验证。

例2:

public class MyCollectorImpl2<T> implements Collector<T, Set<T>, Map<T, T>>{
    @Override
    public Supplier<Set<T>> supplier() {
        return HashSet<T>::new;
    }

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

    @Override
    public BinaryOperator<Set<T>> combiner() {
        return (set1, set2)->{
            set1.addAll(set2);
            return set1;
        };
    }

    @Override
    public Function<Set<T>, Map<T, T>> finisher() {
        return (set)->{
            HashMap<T, T> map = new HashMap();
            set.stream().forEach((item)->map.put(item, item));
            return map;
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.UNORDERED));
    }

    public static void main(String[] args) {
        List<String> list = Arrays.asList("hello", "world", "welcome");
        HashSet<String> set = new HashSet<>();
        set.addAll(list);

        Map<String, String> map = set.stream().collect(new MyCollectorImpl2<>());
        System.out.println(map);
    }
}

这个例子中由于中间累计结果容器的类型与最终类型不一致,在finisher方法中必须做正确的处理,否则肯定抛出类型转换的异常。




以上是关于Java8中重要的收集器Collector的主要内容,如果未能解决你的问题,请参考以下文章

Java 8系列收集器Collector与工具类Collectors

2020了你还不会Java8新特性?Collector类源码分析

Java 8系列Lambda 表达式,一看就废

Java 8系列Stream详解,看这一篇就够啦

Java 8系列全网最通俗易懂的Java8版本新特性知识汇总,看完不懂你捶我

Java 8系列全网最通俗易懂的Java8版本新特性知识汇总,看完不懂你捶我