本文共 7074 字,大约阅读时间需要 23 分钟。
本节介绍两个比较复杂的函数groupingBy, partitioningBy,这两个函数也是通过new CollectorImpl来实现的,分别有多个重载,下面会详细介绍
这个方法可以用来分组,类似于sql中的group by,按照指定的键分组,比如:
Map> groupByCity = people.stream().collect(groupingBy(Person::getCity);
这个操作能将流中的所有people按照city来分组,每个分组是一个List,里面的person对象的city是相同的
这个方法有三个重载方法,前两个比较简单的方法是通过调用第三个方法来实现的,第三个方法才有具体的实现,下面来一一介绍这个方法接受一个Function,传入类型T,返回类型K,这个方法叫classifier,就是用来划分组和组之间的分类器
groupingBy返回的类型是Collector<T, ?, Map<K, List>>,这个collector接受的类型是T,返回的类型是Map<K, List>,Map的key类型是K,value类型是List
举个例子,还是刚刚的代码,这个T类型就是City,K就是PersonMap> groupByCity = people.stream().collect(groupingBy(Person::getCity);
很容易理解,按city分组之后,对于某一组来说,key自然是这个city,value就是这个分组下的Person
这个方法是借助第二个重载方法实现的,详细的在下个小节介绍public staticCollector >>groupingBy(Function classifier) { return groupingBy(classifier, toList());}
这个方法接受两个参数,第一个是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 staticCollector > groupingBy(Function classifier, Collector downstream) { return groupingBy(classifier, HashMap::new, downstream);
这个方法是最复杂的一个重载方法,包含三个参数,其中第一个参数classifier和第三个参数downstream在之前都介绍过了,classifier是用来分组的方法,downstream是分组之后对小组里面的元素进一步处理的collector,而这里的第二个参数是用来决定这个Map的具体类型的。
流中元素的类型 | 分组后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的解释
Supplier
mangledFactory是由第二个参数mapFactory传入后转换的,为什么可以进行强转是因为A和D实际上是同一种类型。
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>,这分为三个步骤通俗的讲,因为downstream是对分组后的“家人“们做处理的,对于流中的每一个元素,要先找到一把钥匙key,回到了自己的家,才能接受downsteam的处理,跟自己的家人一起做运算
这个方法在并行流里面才会调用,由于是并行流,同一个“家庭“的元素被不同的线程处理了,当线程结束的之后,需要把这个分散成几部分的家庭成员的家庭合并起来,也就是根据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合并起来
这个方法是否调用要看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>
这个方法是分区的意思,其实也是一种分组,只不过它只分成两组,符合条件的是一组,不符合条件的是另一组,可以理解成是特殊的groupingBy
这个方法有两个重载,下面分别介绍这个方法接受一个predicate,返回一个Collector,Collector的类型是Collector<T, ?, Map<Boolean, List>>,也就是说这个方法处理后会返回Map<Boolean, List>
由这我们可以看出它的效果是将流中的元素分成两个组,一组是满足predicate的,用key=true可以拿到,另一组是不满足predicate的,用key=false可以拿到这个方法是调用第二个重载方法实现的,我们将在下一小节介绍
这个方法接受两个参数,一个是predicate,另一个是downstream,其实跟groupingBy的设计思路是很像的,predicate用来分区,分完区后对于区中的List再进行一次处理,最终返回的Collector是Collector<T, ?, Map<Boolean, D>>
流中元素的类型 | 分组后key的类型 | downstream处理的元素类型 | downstream的中间结果容器类型 | downstream的返回类型 |
---|---|---|---|---|
T | Boolean | T | A | D |
但是由于分区后是个Map,而且只有两个分区,因此这个方法借助了一个内部类Partition来实现,可以略看一下这个内部类的组成
private static final class Partitionextends AbstractMap implements Map { final T forTrue; final T forFalse; Partition(T forTrue, T forFalse) { this.forTrue = forTrue; this.forFalse = forFalse; } }
所以中间的结果容器会涉及这个类的实现
Supplier> supplier = () -> new Partition<>(downstream.supplier().get(), downstream.supplier().get());
容器是一个Partition对象里面封装着downstream的容器
BiConsumer downstreamAccumulator = downstream.accumulator(); BiConsumer, T> accumulator = (result, t) -> downstreamAccumulator.accept(predicate.test(t) ? result.forTrue : result.forFalse, t);
先用predicate找到钥匙true后者false,然后得到对应的容器,然后再将当前元素和容器做一个accumulate的操作
跟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))
这个方法也要看downstream的characteristic的组成,跟groupingBy是一样的
这两个方法的设计思想还是很像的,他们的重载方法中都是参数少是常用的,是通过调用参数多的方法来实现的,参数最多的方法会有完全的实现
细读最复杂的方法能发现collector的组成,看代码的时候按照元素的类型变化来理解会比较容易 而且downstream的这个参数的设计在两个方法来说都很统一,是对分组/分区后每个小集合的元素的一种继续处理转载地址:http://qjvzb.baihongyu.com/