groupingBy
方法有多个重载方法,但是根本上只有一个方法。之所以提供这么多方法的重载,主要目的还是为了开发者调用方便。通过对于此分组静态方法的学习,我们可以更好地了解Java在收集器collector
接口实现上的设计模式以及设计思想。
CodeBlock-1:
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
这是groupingBy
方法的重载版本之一,实际上其调用了重载版本2的代码块,即 CodeBlock-2: 其输入参数只有分类器接口的实例,所以中间容器被默认限制为ArrayList
类型。
CodeBlock-2:
public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream) {
return groupingBy(classifier, HashMap::new, downstream);
}
这是groupingBy
方法的第二个重载版本,其输入参数有分类器实例classifier
,下流收集器实例downstream
,其实际上调用了 CodeBlock-3: 中的第三个重载版本,所以具体如何实现的我们放到 CodeBlock-3: 中进行分析。
CodeBlock-3: 注意: 我几乎对源码每句/块语句都进行了分析,而分析的注释都是在语句/块的下方。
public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream) {
Supplier<A> downstreamSupplier = downstream.supplier();
//D得到下流收集器的supplier对象,其提供了下流收集器的结果容器类型A
BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
//获得下流收集器的累加器接口实现对象。
BiConsumer<Map<K, A>, T> accumulator = (m, t) -> {
//不加任何前缀的accumulator接口实现对象是指整个groupingBy方法返回的收集器的accumulator接口实现对象,
//其通过classifier(Function)、downstream(Collector)一起来构建的:构建的累加器使中间结果类型为:Map<K,A>,
K key = Objects.requireNonNull(classifier.apply(t), "element cannot be mapped to a null key");
//得到一个对象的键值,且规定键值不能为空
A container = m.computeIfAbsent(key, k -> downstreamSupplier.get());
//这是A类型的中间容器,我们对键值进行判断,如果存在与key对应的值A,即A类中间结果容器不为null,
//则返回其对应(注意:这里已经是返回元素所对应分类组别的中间容器,所以每个元素都要进行一次累加操作)的中间结果容器A(Map<K, A>),
//如果没有对应的结果容器,则返回一个新的空结果容器,并将key值和新的结果容器放入此map中,并且返回这个结果容器。
downstreamAccumulator.accept(container, t);
//这里调用了下流传递过来的累加方法,可见其实现的操作是将元素添加到经根据Key值分类的中间结果容器中。
};
BinaryOperator<Map<K, A>> merger = Collectors.<K, A, Map<K, A>>mapMerger(downstream.combiner());
//此方法主要作用是合并两个map,并处理重复键
@SuppressWarnings("unchecked")
Supplier<Map<K, A>> mangledFactory = (Supplier<Map<K, A>>) mapFactory;
//这里将输入参数中的supplier对象强制类型转换能够一定保证不报错,这是因为整个分组方法的中间结果容器类型就是Supplier<Map<K, A>>类型,
//而参数中的supplier接口目的本身就是如此,提供整个接口实现的中间结果容器。
//而且注意,这里只是说传递给新的引用变量,使mangledFactory作为整个方法的返回的接口实例中的supplier实现。
//还有一个深层次原因,因为Supplier<Map<K, A>>和最终结果容器Supplier<Map<K, D>>,
//其实就是差一步finisher的操作,所以强制类型转换就是把Value值改变了一下,但是不影响map这个整体框架。
//所有强制类型转换真的就是真的只是类型转换所以才可以转。
if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
return new CollectorImpl<>(mangledFactory, accumulator, merger, CH_ID);
}
//如果此方法返回的collector接口有Collector.Characteristics.IDENTITY_FINISH特性,那么就跳过finisher,
//直接返回累加器就结束了,最终结果容器类型为:Map<K, A>
else {
@SuppressWarnings("unchecked")
Function<A, A> downstreamFinisher = (Function<A, A>) downstream.finisher();
//强制类型转换能够实现的理由还是A->D也就一个finisher操作,map的框架并没有改变
Function<Map<K, A>, M> finisher = intermediate -> {
//其实此处的目的很直接,就是实现整个分组方法的finisher接口实现,从Map<K, A>转变为M extends Map<K, D>
intermediate.replaceAll((k, v) -> downstreamFinisher.apply(v));
//replaceAll方法是Map类的方法,其方法就是保留key,但是将value通过Function接口实现替换掉功能,这里的语句当然不是马上执行,
//而是为了实现finisher接口穿进去lambda表达式而已,目前为止已经实现Map<K, A>到Map<K,D>的转化,但是不要急于返回。
@SuppressWarnings("unchecked")
M castResult = (M) intermediate;
//这所以要强制类型砖转换,这是因为分组方法的总体返回是M extends Map<K, D>,而不是Map<K, D>,
//当然如果M extends Map<K, D> 不对,就会产生强制类型转换的错误
return castResult;
};
return new CollectorImpl<>(mangledFactory, accumulator, merger, finisher, CH_NOID);
//可见最终的分组也是返回一个collector对象实现,只不过其内部逻辑比一般的数据结构的简单转换更为复杂。
}
}
如果你觉得上述分析有难度,不妨先看这里的设计思想在返回2中进行源码阅读,又或者你lambda
表达式和方法引用还未学习,在看完设计模式后,在看看我写关于Lambda
表达式以及方法引用的博文,在回来看2中的源码也是一个循序渐进的好选择。
classifier(Function)
找到当前元素所对应的键值,用于分类
1). 如果当前键值对应的组别已有中间结果容器A,那么就将当前元素加入此中间结果容器A中,并且将中间结果容器放到Map<K,A>
2) 如果没有,则创建与当前元素键值对应的中间结果容器,并将当前元素放入此容器中;map<K,A>
->然后如果有多线程的话,就进行对个map对象的合并,否则无序调用合并器的方法Characteristics.IDENTITY
,那么就调用finisher接口进行中间结果容器的类型转化map<K,D>
,最后强制类型转换为Map<K,M>
返回以上泛型符号类型说明:
T:流中的单个元素类型
K:元素进行分类的属性,最终Map数据结构的键值Key
D:downstrream下流收集器对象的最终结果容器
A:downstrream下流收集器对象的中间结果容器
M:M extends Map<K, D>,其为整个分类方法的中间结果容器
源码中接重要收集器接口对象的作用说明:
1)classifier提供一个接口实现,从对象中提取属性,返回键值K:算是从T类型的元素对象中提取相关的属性值K,用来进行分组、分类的判断依据 此方法针对于流的单个元素
2)mapFactory提供一个最大的框架M:抽象接口Map的实现:HashMap或者TreeMap 此方法是无参的!
3)downstream提供了一个接口与实现,提供一种方法:可将相同分类组的对象用一个结果容器存放,并返回D 此方法也针对流的单个元素
流中处理元素的核心思想: 流的操作都是Lazy的,不可能像一批产品的流水线一样完成(毕竟工人,即处理器核心数,没有那么多),而是一个元素进行一整套完整的工作流程,至少一步到位地运行到将其放到方法整体的中间结果容器中。