Collector和Collectors类(收集器原理分析)


前言

Stream流中的collect方法,收集一个Collector实例,将流中的元素收集成另一种数据结构:
<R, A> R collect(Collector<? super T, A, R> collector);

  • collect是Stream流的一个 终止方法,会使用传入的收集器(Collector实现对象)对结果执行相关的操作。
  • Collector是一个接口,专门作为collect方法的入参,也就是收集器。
  • Collectors是生产Collector收集器对象的工具类。

一、Collector接口

Collector接口(收集器)以下5个方法用来生成5个成员变量:

public interface Collector<T, A, R> {
	//供给型(Supplier)参数,supplier用于生成A类型的结果容器
    Supplier<A> supplier();
	//消费型(Consumer)参数,accumulator用于消费元素,也就是归纳元素,这里的T就是元素,它会将流中的元素一个一个与结果容器A发生操作
    BiConsumer<A, T> accumulator();
	//方法型(Function)参数,combiner用于两个两个合并并行执行的线程的执行结果,将其合并为一个最终结果A
    BinaryOperator<A> combiner();
	//方法型(Function)参数,finisher用于将之前整合完的结果A转换成为R
    Function<A, R> finisher();
	//characteristics表示当前Collector的特征值,这是个不可变Set
    Set<Characteristics> characteristics();
}

characteristics是一个内部枚举类,包含下面三个值:

public interface Collector<T, A, R> {

	//内部枚举类
    enum Characteristics {
		//多线程并行: 声明此收集器可以多个线程并行处理,允许并行流中进行处理
        CONCURRENT,
		//无序: 声明此收集器的汇总归约结果与Stream流元素遍历顺序无关,不受元素处理顺序影响
        UNORDERED,
		//无需转换结果: 声明此收集器的finisher方法是一个恒等操作,可以跳过
        IDENTITY_FINISH
    }
}

两个of重载方法,用于生成Collector实例,一个包含全部的参数,另一个不包含finisher参数:

public interface Collector<T, A, R> {

  //四个参数的of方法,生成一个Collector对象,T表示流中的元素,R表示最终结果,也就是容器
  public static<T, R> Collector<T, R, R> of(Supplier<R> supplier,
                                              BiConsumer<R, T> accumulator,
                                              BinaryOperator<R> combiner,
                                              Characteristics... characteristics) {
        Objects.requireNonNull(supplier);
        Objects.requireNonNull(accumulator);
        Objects.requireNonNull(combiner);
        Objects.requireNonNull(characteristics);
        Set<Characteristics> cs = (characteristics.length == 0)
                                  ? Collectors.CH_ID
                                  : Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH, characteristics));
        return new Collectors.CollectorImpl<>(supplier, accumulator, combiner, cs);
    }

    // //四个参数的of方法,生成一个Collector对象,T表示流中的元素,R表示最终结果,finisher用于将中间结果A转换成R
    public static<T, A, R> Collector<T, A, R> of(Supplier<A> supplier,
                                                 BiConsumer<A, T> accumulator,
                                                 BinaryOperator<A> combiner,
                                                 Function<A, R> finisher,
                                                 Characteristics... characteristics) {
        Objects.requireNonNull(supplier);
        Objects.requireNonNull(accumulator);
        Objects.requireNonNull(combiner);
        Objects.requireNonNull(finisher);
        Objects.requireNonNull(characteristics);
        Set<Characteristics> cs = Collectors.CH_NOID;
        if (characteristics.length > 0) {
            cs = EnumSet.noneOf(Characteristics.class);
            Collections.addAll(cs, characteristics);
            cs = Collections.unmodifiableSet(cs);
        }
        return new Collectors.CollectorImpl<>(supplier, accumulator, combiner, finisher, cs);
    }

}

这里拿大神文章的一个图来看,可以清晰的看明白处理流程及Collector这5个参数的作用,分串行和并行两种情况,如下:
在这里插入图片描述
在这里插入图片描述
原文连接:https://cloud.tencent.com/developer/article/2066869

二、Collectors工具类

1,简介

Collectors是一个工具类,是JDK预实现Collector的工具类,它内部提供了多种收集器的生成策略,我们可以直接拿来使用,非常方便。

2,收集器

根据操作类型,可以将收集器分成下面三个类:


  • 恒等处理: 该类操作元素本身没有变化
    • toList()
    • toSet()
    • toCollection(Supplier<C> collectionFactory)
    • toMap
  • 归约处理: 该类操作将流中的元素进行处理,最后得到一个最终结果
    • counting()
    • summingInt(ToIntFunction<? super T> mapper)
    • maxBy(Comparator<? super T> comparator)
    • minBy(Comparator<? super T> comparator)
    • joining()
  • 分组分区: 按照某个维度将stream流中的元素进行分组
    • groupingBy(Function<? super T, ? extends K> classifier)
    • partitioningBy(Predicate<? super T> predicate)

(1)、恒等处理

Stream中的元素经过Collector收集器处理前后完全不变,拿toList()来看,该收集器只是将结果取出,放到List集合中,本质没有对元素本身有任何修改。

来看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);
    }

在这里插入图片描述

来看上面的toList方法,该方法返回了一个Collector的实现类CollectorImpl,CollectorImpl是该工具类中的一个收集器的实现类:
suppllier为结果容器,这里就是new出来的ArrayList集合对象,A为List<T>;
accumulator为list::add,即将流中的元素T与容器A进行操作,这里的操作就是将元素添加到容器中;
combiner为(left, right) -> { left.addAll(right); return left; },也就是合并操作,将一个结果right添加到一个结果left中,针对并行操作有效;
finisher这里没用到这个参数;
characteristics为IDENTITY_FINISH,即无需转换结果。

演示:

@Test
    public void test1(){
        String[] arr = new String[]{"a","bb","ccc","d"};

        //toCollection   接收一个Supplier类型参数,用作自定义的容器  toCollection(Supplier<C> collectionFactory)
        ArrayList<String> collect = Arrays.stream(arr).collect(Collectors.toCollection(ArrayList::new));
        System.out.println(collect); //[a, bb, ccc, d]

        //toList
        List<String> list = Arrays.stream(arr).collect(Collectors.toList());
        System.out.println(list); //[a, bb, ccc, d]

        //toSet
        Set<Integer> set = Arrays.stream(arr).map(String::length).collect(Collectors.toSet());
        System.out.println(set); //[1, 2, 3]

        //toMap, 保证key唯一,否则报错
        Map<String, Integer> map = Arrays.stream(arr).collect(Collectors.toMap(Function.identity(), String::length));
        System.out.println(map); //{bb=2, a=1, ccc=3, d=1}

        //toMap, key重复,自定义策略,(o,o1)->o1:原来值为o,现在值为o1,取o1,即新值覆盖
        Map<Integer, String> map1 = Arrays.stream(arr)
                .collect(Collectors.toMap(String::length, Function.identity(),(o,o1)->o1));
        System.out.println(map1); //{1=d, 2=bb, 3=ccc}
    }

(2)、归约处理

Stream流中的元素经过收集器处理后变成一个结果,比如累加,计算等等。。。

先来看归约方法,同Stream流中的三个重载的归约方法,Collectors中也提供了同样的三个reducing方法,如下:

    public void test2(){
        Integer[] arr = {1,2,3};

        //一个参数
        Optional<Integer> collect = Arrays.stream(arr).collect(Collectors.reducing(Integer::sum));
        System.out.println(collect.get()); //6

        //两个参数,有初始值
        Integer collect1 = Arrays.stream(arr).collect(Collectors.reducing(10, Integer::sum));
        System.out.println(collect1);  //16

        //三个参数,包含合并方法  非并行  10+1+2+3   区别于Stream中的三个参数的方法,这里第二个参数是Function类型,非BiFunction
        Integer collect2 = Arrays.stream(arr).collect(Collectors.reducing(10, (x) -> x, (x, y) -> x + y));
        System.out.println(collect2);  //16
    }

拿一个参数的reducing源码来看,底层逻辑也是收集器的实现
在这里插入图片描述
两个参数的归约方法:
在这里插入图片描述
这里可以看到两个参数的归约方法和一个参数的方法容器使用不同,对于一个参数来说,为什么不能直接使用数组作为容器呢,还要单独new一个新的内部类?对于两参方法,提供了初始值,可直接将初始值作为开始元素存入数组第一个索引位,后面直接拿该元素位的值与流元素T发生操作,但是对于一参无初始化值,如果也同二参这样拿数组第一个元素位记录,那么初始化时数组第一个元素位并不能准确的赋值,如果是null,后面accept方法可能报错,如果默认0,如果求累计积则结果也不符合预期。而内部类这个对象通过present能将流中的第一个t赋值到value,后续只需拿value同t进行消费方法的操作即可。

三个参数的归约方法:
在这里插入图片描述

maxBy、minBy、counting底层用的也是reducing,用法如下:
在这里插入图片描述

    @Test
    public void test9(){
        List<String> list = Arrays.asList("3", "7", "1", "3");

		//计数
        Long collect = list.stream().collect(Collectors.counting());
        System.out.println(collect); //4

		//最值
        Optional<String> max = list.stream().collect(Collectors.maxBy(String::compareTo));
        System.out.println(max.get()); //7
        Optional<String> min = list.stream().collect(Collectors.minBy(String::compareTo));
        System.out.println(min.get()); //1

		//平均数
        Double avg = list.stream().collect(Collectors.averagingInt(Integer::parseInt));
        System.out.println(avg); //3.5     -->  (14 / 4 )

		//summingInt
		Integer collect1 = list.stream().collect(Collectors.summingInt(String::length));
        System.out.println(collect); //4
    }

再来看summingInt方法,入参为一个将T转换为Int输出的方法型对象。其实现同理也是返回了收集器实现对象,初始容器为一个长度为1的数组,拿该数组中仅有的一个元素(a[0])与目标元素依次求和,合并方法为a[0]+b[0];finisher这里将中间结果R(a数组对象)转换成了A(int类型)输出。

    public static <T> Collector<T, ?, Integer> summingInt(ToIntFunction<? super T> mapper) {
        return new CollectorImpl<>(
                () -> new int[1],
                (a, t) -> { a[0] += mapper.applyAsInt(t); },
                (a, b) -> { a[0] += b[0]; return a; },
                a -> a[0], CH_NOID);
    }

同理averagingInt底层实现同样如此,区别是使用数组第二个索引位记录个数,依次累加:

    public static <T> Collector<T, ?, Double>  averagingInt(ToIntFunction<? super T> mapper) {
        return new CollectorImpl<>(
                () -> new long[2],
                (a, t) -> { a[0] += mapper.applyAsInt(t); a[1]++; },
                (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; },
                a -> (a[1] == 0) ? 0.0d : (double) a[0] / a[1], CH_NOID);
    }

joining底层收集器如下:

    public static Collector<CharSequence, ?, String> joining() {
        return new CollectorImpl<CharSequence, StringBuilder, String>(
                StringBuilder::new, StringBuilder::append,
                (r1, r2) -> { r1.append(r2); return r1; },
                StringBuilder::toString, CH_NOID);
    }

(3)、分组分区

分组:

先来看groupingBy的源码,该方法需要两个关键参数:

  • 一个是分组函数,用来判断如何分组,即key的取值;
  • 一个是值收集器,也就是用来存放分组后的value值的容器。

我们先看一个参数的groupingBy,如下:
在这里插入图片描述
这里分组函数为形参中自定义的方法型参数,值收集器为toList()方法返回的收集器对象,在往下看,调用了两个参数的groupingBy方法:
在这里插入图片描述

这里调用了三个参数的groupingBy方法,mapFactory为生产结果容器的供给型参数。详细分组源码过程如下图所示:
在这里插入图片描述

    @Test
    public void test3(){
        List<String> list = Arrays.asList("aa", "b", "ac", "bdd", "ae", "aff", "bg");
        //字符长度分组
        Map<Integer, List<String>> map = list.stream().collect(Collectors.groupingBy(String::length));
        System.out.println(map);//{1=[b], 2=[aa, ac, ae, bg], 3=[bdd, aff]}
}

来看这个例子,根据String长度分组来举例,具体的执行流程如下图所示:
在这里插入图片描述
叠加嵌套
上面的值收集器默认使用的是toList()方法返回的收集器实现对象,来看下面的一个例子:

        //两个分组条件:先根据字符首字母分组,然后根据字符长度分组
        Map<Character, Map<Integer, List<String>>> collect = list.stream()
                .collect(Collectors.groupingBy(s -> s.charAt(0), Collectors.groupingBy(String::length)));
        System.out.println(collect); //{a={2=[aa, ac, ae], 3=[aff]}, b={1=[b], 2=[bg], 3=[bdd]}}

这里根据两个条件分组,先根据首字母分组,然后再根据字符长度分组,类比于上面根据String长度来分组的流程图,我们可以想到的是,基本执行流程相同,不同的是对于收集器,这里使用的并不是toList()方法默认的ArrayList的结果容器,这里第一次分组使用的分组函数是首字母,如a ->{aa,ac,ae,af} ,值收集器使用的是根据字符长度来分组返回的收集器,也就是这部分结果{aa,ac,ae,af}调用收集器的消费对象方法(长度分组)与收集器的容器对象(Map)发生操作,结果为:2=[aa, ac, ae], 3=[aff],将这部分的收集器对象当做第一次分组的值收集器对象放入,最终值就是:{a= {2=[aa, ac, ae], 3=[aff]} }。

分区:
按照指定条件将 stream 划分为 true 和 false 两个 map,先来看partitioningBy一个参数的源码:
在这里插入图片描述
这里看到类似于分组,依旧是默认使用的收集器是toList()
在这里插入图片描述
内部类Partition如下:
在这里插入图片描述

实例演示:

    @Test
    public void test10(){
        List<String> list = Arrays.asList("aa", "b", "ac", "bdd", "ae", "aff", "bg");
  
        Map<Boolean, List<String>> collect1 = list.stream()
                .collect(Collectors.partitioningBy(s -> s.length() > 2));
        System.out.println(collect1); //{false=[aa, b, ac, ae, bg], true=[bdd, aff]}
    }

3,collectingAndThen

包裹另一个收集器,对其结果进行二次加工转换,源码如下:

在这里插入图片描述

例如:

        String[] arr = new String[]{"a","bb","ccc","d"};

        //toList
        List<String> list = Arrays.stream(arr).collect(Collectors.toList());
        System.out.println(list); //[a, bb, ccc, d]

		//collectingAndThen包裹,finisher为List::size,也就是本来输出的R为数组,现在将R为参数,通过入参的finisher.apply将其长度输出。
        Integer collect1 = Arrays.stream(arr).collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
        System.out.println(collect1); //4
        //两个参数归约方法,本身的finisher将初始值 数组a -> a[0]
        Integer collect1 = Arrays.stream(arr).collect(Collectors.reducing(10, Integer::sum));
        System.out.println(collect1);  //16

		//collectingAndThen包裹后,也就是将原本的int型16转为String类型
        String collect3 = Arrays.stream(arr).collect(Collectors.collectingAndThen(Collectors.reducing(10, Integer::sum), x -> x + ""));
        System.out.println(collect3);

参考文章
https://www.bbsmax.com/A/lk5amVPa51/
https://cloud.tencent.com/developer/article/2066869

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值