博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[Java8新特性]Collectors源码阅读-3 groupingBy和partitioningBy
阅读量:2168 次
发布时间:2019-05-01

本文共 7074 字,大约阅读时间需要 23 分钟。

本节介绍两个比较复杂的函数groupingBy, partitioningBy,这两个函数也是通过new CollectorImpl来实现的,分别有多个重载,下面会详细介绍

1.groupingBy

这个方法可以用来分组,类似于sql中的group by,按照指定的键分组,比如:

Map
> groupByCity = people.stream().collect(groupingBy(Person::getCity);

这个操作能将流中的所有people按照city来分组,每个分组是一个List,里面的person对象的city是相同的

这个方法有三个重载方法,前两个比较简单的方法是通过调用第三个方法来实现的,第三个方法才有具体的实现,下面来一一介绍

1.1 groupingBy(Function<? super T, ? extends K> classifier)

这个方法接受一个Function,传入类型T,返回类型K,这个方法叫classifier,就是用来划分组和组之间的分类器

groupingBy返回的类型是Collector<T, ?, Map<K, List>>,这个collector接受的类型是T,返回的类型是Map<K, List>,Map的key类型是K,value类型是List

举个例子,还是刚刚的代码,这个T类型就是City,K就是Person

Map
> groupByCity = people.stream().collect(groupingBy(Person::getCity);

很容易理解,按city分组之后,对于某一组来说,key自然是这个city,value就是这个分组下的Person

这个方法是借助第二个重载方法实现的,详细的在下个小节介绍

public static 
Collector
>>groupingBy(Function
classifier) {
return groupingBy(classifier, toList());}

1.2 groupingBy(Function<? super T, ? extends K> classifier,Collector<? super T, A, D> downstream)

这个方法接受两个参数,第一个是classifier,第二个参数是一个Collector,叫做downstream,顾名思义,就是一个下游的Collector,这里我们可以理解为分完组之后对组中的元素进行的后续处理。

还是刚刚的例子,对于上面的那个方法分组后得到Map<City, List<Person>,downStream可以对List<Persion>进行二次处理,比如如果想让people这个list在按City分组之后,取每个person 的lastName,并且希望lastName的容器类型由List变成Set,可以这样做:

Map
> namesByCity = people.stream().collect(groupingBy(Person::getCity, mapping(Person::getLastName, toSet())));

后面的mapping方法其实是返回一个Collector,这个collector在处理流中每个元素Person的时候,会先做一个映射,将person转成lastName,然后再将流放入一个set中,等同于:

personsByCity.stream()   	.map(Person::getLastname)   	.collect(toSet());

personsByCity 指的是分组后里面的一个小组,downStream就是对这部分进行处理

在这里插入图片描述
它是借助第三个重载方法实现的,详细的在下个小节介绍

public static 
Collector
> groupingBy(Function
classifier, Collector
downstream) {
return groupingBy(classifier, HashMap::new, downstream);

1.3 groupingBy(Function<? super T, ? extends K> classifier, Supplier mapFactory, Collector<? super T, A, D> downstream)

这个方法是最复杂的一个重载方法,包含三个参数,其中第一个参数classifier和第三个参数downstream在之前都介绍过了,classifier是用来分组的方法,downstream是分组之后对小组里面的元素进一步处理的collector,而这里的第二个参数是用来决定这个Map的具体类型的。

1.3.1方法的泛型类型
流中元素的类型 分组后key的类型 mapFactory的类型 downstream处理的元素类型 downstream的中间结果容器类型 downstream的返回类型
T K M T A D

最后方法返回一个collector,类型是Collector<T, ?, M>,其中M的类型是M extends Map<K, D>

意思是整个groupingBy方法的效果是:
对于流中的T类型元素,返回一个Map<K, D>类型,其中K是分组的键的类型
可以借助下面的图进行理解
在这里插入图片描述
具体实现我们再慢慢分析,源代码比较长,我们在这里按照Collector的组成拆开来看
先看一个概览,整体的Collector的组成的职责是:

supplier accumulator combiner finisher characteristic
提供Map<K,A>容器 对于流中的元素T,先映射到K,用K去找到对应的Map<K,A>容器,将这个结果加入到这个容器 将中间结果容器Map<K,A>进行merge操作 将A类型转换成D类型 根据downstream来决定

下面是这个综合collector的解释

1.3.2 supplier
Supplier
> mangledFactory = (Supplier
>) mapFactory;

mangledFactory是由第二个参数mapFactory传入后转换的,为什么可以进行强转是因为A和D实际上是同一种类型。

1.3.3 accumulator:
BiConsumer
, T> accumulator = (m, t) -> {
K key = Objects.requireNonNull(classifier.apply(t), "element cannot be mapped to a null key"); A container = m.computeIfAbsent(key, k -> downstreamSupplier.get()); downstreamAccumulator.accept(container, t);};

accumulator是从downstream组装的,其实它只组装到Map<K,A>类型,从Map<K, A>到Map<K, D>类型是finisher做的

对应到上图中,就是从List<T>转换到Map<K, A>,这分为三个步骤

  • 首先要进行分类,也就是T转到K,得到key
  • 然后在中间结果容器m中,用key找到对应的子容器container,如果没有找到的话,就提供一个空的容器k -> downstreamSupplier.get()
  • 第二步得到的container对于downstream来说算是中间结果容器,再和当前元素t做运算。

通俗的讲,因为downstream是对分组后的“家人“们做处理的,对于流中的每一个元素,要先找到一把钥匙key,回到了自己的家,才能接受downsteam的处理,跟自己的家人一起做运算

1.3.4 combiner

这个方法在并行流里面才会调用,由于是并行流,同一个“家庭“的元素被不同的线程处理了,当线程结束的之后,需要把这个分散成几部分的家庭成员的家庭合并起来,也就是根据key去合并,key相同的合并成一个小集合。

BinaryOperator
> merger = Collectors.
>mapMerger(downstream.combiner());

这个mapMerger的实现是:

private static 
> BinaryOperator
mapMerger(BinaryOperator
mergeFunction) {
return (m1, m2) -> {
for (Map.Entry
e : m2.entrySet()) m1.merge(e.getKey(), e.getValue(), mergeFunction); return m1; };

作用就是将相同的key的value合并起来

1.3.5 finisher

这个方法是否调用要看downstream的characteristic的组成,如果里面有IDENTITY_FINISH,那就是不执行finisher了,如果不包含IDENTITY_FINISH,就会整合downstream的finisher

finisher就是从Map<K, A>转成Map<K,D>的方法

Function
downstreamFinisher = (Function
) downstream.finisher(); Function
, M> finisher = intermediate -> {
intermediate.replaceAll((k, v) -> downstreamFinisher.apply(v)); @SuppressWarnings("unchecked") M castResult = (M) intermediate; return castResult; };

是用downstream的finisher方法对Map里面分元素进行一个转换,intermediate的类型是Map<K,A>,这个lambda表达式只是将intermediate里面的value都进行一个替换,然后返回这整个intermediate,这里也有个强转是因为A和D是同一种类型,所以M就是Map<K,A>

2.partitioningBy

这个方法是分区的意思,其实也是一种分组,只不过它只分成两组,符合条件的是一组,不符合条件的是另一组,可以理解成是特殊的groupingBy

这个方法有两个重载,下面分别介绍

2.1 partitioningBy(Predicate<? super T> predicate)

这个方法接受一个predicate,返回一个Collector,Collector的类型是Collector<T, ?, Map<Boolean, List>>,也就是说这个方法处理后会返回Map<Boolean, List>

由这我们可以看出它的效果是将流中的元素分成两个组,一组是满足predicate的,用key=true可以拿到,另一组是不满足predicate的,用key=false可以拿到
在这里插入图片描述

这个方法是调用第二个重载方法实现的,我们将在下一小节介绍

2.2 partitioningBy(Predicate<? super T> predicate, Collector<? super T, A, D> downstream)

这个方法接受两个参数,一个是predicate,另一个是downstream,其实跟groupingBy的设计思路是很像的,predicate用来分区,分完区后对于区中的List再进行一次处理,最终返回的Collector是Collector<T, ?, Map<Boolean, D>>

2.2.1方法的泛型类型
流中元素的类型 分组后key的类型 downstream处理的元素类型 downstream的中间结果容器类型 downstream的返回类型
T Boolean T A D

但是由于分区后是个Map,而且只有两个分区,因此这个方法借助了一个内部类Partition来实现,可以略看一下这个内部类的组成

private static final class Partition
extends AbstractMap
implements Map
{
final T forTrue; final T forFalse; Partition(T forTrue, T forFalse) {
this.forTrue = forTrue; this.forFalse = forFalse; } }

所以中间的结果容器会涉及这个类的实现

2.2.2 supplier
Supplier
> supplier = () -> new Partition<>(downstream.supplier().get(), downstream.supplier().get());

容器是一个Partition对象里面封装着downstream的容器

2.2.3 accumulator:
BiConsumer
downstreamAccumulator = downstream.accumulator(); BiConsumer
, T> accumulator = (result, t) -> downstreamAccumulator.accept(predicate.test(t) ? result.forTrue : result.forFalse, t);

先用predicate找到钥匙true后者false,然后得到对应的容器,然后再将当前元素和容器做一个accumulate的操作

2.2.4 combiner

跟groupingBy的思路没有什么不同,只是数据结构不同了,在合并的时候要进行归类,因为数据结构比较简单,所以这里也比较好理解,找到钥匙之后,两个partition里面的true的部分放入新的partition的true部分,false的也是一样的。

BinaryOperator op = downstream.combiner();        BinaryOperator
> merger = (left, right) -> new Partition<>(op.apply(left.forTrue, right.forTrue), op.apply(left.forFalse, right.forFalse))
1.3.5 finisher

这个方法也要看downstream的characteristic的组成,跟groupingBy是一样的

3.总结

这两个方法的设计思想还是很像的,他们的重载方法中都是参数少是常用的,是通过调用参数多的方法来实现的,参数最多的方法会有完全的实现

细读最复杂的方法能发现collector的组成,看代码的时候按照元素的类型变化来理解会比较容易
而且downstream的这个参数的设计在两个方法来说都很统一,是对分组/分区后每个小集合的元素的一种继续处理

转载地址:http://qjvzb.baihongyu.com/

你可能感兴趣的文章
【LEETCODE】215-Kth Largest Element in an Array
查看>>
【LEETCODE】241-Different Ways to Add Parentheses
查看>>
【LEETCODE】312-Burst Balloons
查看>>
【LEETCODE】232-Implement Queue using Stacks
查看>>
【LEETCODE】225-Implement Stack using Queues
查看>>
【LEETCODE】155-Min Stack
查看>>
【LEETCODE】20-Valid Parentheses
查看>>
【LEETCODE】290-Word Pattern
查看>>
【LEETCODE】36-Valid Sudoku
查看>>
【LEETCODE】205-Isomorphic Strings
查看>>
【LEETCODE】204-Count Primes
查看>>
【LEETCODE】228-Summary Ranges
查看>>
【LEETCODE】27-Remove Element
查看>>
【LEETCODE】66-Plus One
查看>>
【LEETCODE】26-Remove Duplicates from Sorted Array
查看>>
【LEETCODE】118-Pascal's Triangle
查看>>
【LEETCODE】119-Pascal's Triangle II
查看>>
【LEETCODE】88-Merge Sorted Array
查看>>
【LEETCODE】19-Remove Nth Node From End of List
查看>>
【LEETCODE】125-Valid Palindrome
查看>>