java8三次分组_java8实战:使用流收集数据之toList、joining、groupBy(多字段分组)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/prestigeding/article/details/91355293

java8专栏目录:

本文将从Collectos中构建收集器入手,详细介绍java8提供了哪些收集器,重点介绍:toList、toSet、toCollection、joining、groupBy(包含多级分组)、reducing的核心实现原理与使用示例。

本节目录

1、toList、toSet、toCollection

首先对流中的数据进行计算,最终返回的数据类型为集合。Collectors中定义了如下3集合类收集器,其声明如下:

public static Collector> toList()

public static Collector> toSet()

public static > Collector toCollection(Supplier collectionFactory)

温馨提示:建议根据上篇的理论,再来反推一下这些Collector中的核心属性的值,例如supplier、accumulator、combiner、characteristics。不过特别注意,toList、toCollection是不支持并行运行的,但toSet()方法支持并行运行。

我们首先来看一个一直使用的示例,返回菜单中所有菜品的名称:

public static void test_toList(List menu) {

List names = menu.stream().map(Dish::getName)

.collect(Collectors.toList());

}

由于toList方法的实现原理已经在 java8读书笔记:探究java8流收集数据原理中也详细介绍,故本篇不再重点介绍。

2、joining

Collectors定义了如下3个重载方法。

public static Collector joining()

public static Collector joining(CharSequence delimiter)

public static Collector joining(CharSequence delimiter,

CharSequence prefix, CharSequence suffix)

2.1 joining

public static Collector joining() {

return new CollectorImpl(

StringBuilder::new, StringBuilder::append,

(r1, r2) -> { r1.append(r2); return r1; },

StringBuilder::toString, CH_NOID);

}

Supplier< A> supplier()

其函数为StringBuilder::new,即通过该方法创建一个StringBuilder方法,作为累积器的初始值。

BiConsumer accumulator

累积器:StringBuilder::append,即会对流中的元素执行追加。

BinaryOperator< A> combiner

组合器,也是调用append方法,进行字符串的规约。

Function finisher

转换器:由于累积器返回的最终对象为StringBuilder,并不是目标String类型,故需要调用StringBuilder#toString方法进行转换

Set< Characteristics> characteristics

无任何行为。

从上面的函数定义我们可以得出该方法的作用:针对字符串流,会对流中的元素执行字符的追加动作,流元素之间没有分隔符号,示例如下:

6d9a94334d2a70dfdd04e98146cee30c

2.2 joining(CharSequence delimiter)

public static Collector joining(CharSequence delimiter) {

return joining(delimiter, "", "");

}

public static Collector joining(CharSequence delimiter,

CharSequence prefix,

CharSequence suffix) {

return new CollectorImpl<>(

() -> new StringJoiner(delimiter, prefix, suffix),

StringJoiner::add, StringJoiner::merge,

StringJoiner::toString, CH_NOID);

}

Supplier< A> supplier()

其函数为() -> new StringJoiner(delimiter, prefix, suffix),累积器的初始值为StringJoiner。

BiConsumer accumulator

累积器:StringJoiner::append,即会对流中的元素执行追加。

BinaryOperator< A> combiner

组合器,StringJoiner::merge。

Function finisher

转换器:由于累积器返回的最终对象为StringBuilder,并不是目标String类型,故需要调用StringBuilder#toString方法进行转换

Set< Characteristics> characteristics

无任何行为。

其示例如下:

4665f9912e0773eafebf37e5b2e43a5c

3、聚合相关收集器

聚合相关收集器,主要包括minBy、maxBy、sum、avg等相关函数,其主要方法声明如下:

public static Collector> minBy(Comparator super T> comparator)

public static Collector> maxBy(Comparator super T> comparator)

public static Collector summingInt(ToIntFunction super T> mapper)

public static Collector summingLong(ToLongFunction super T> mapper)

public static Collector summingDouble(ToDoubleFunction super T> mapper)

public static Collector averagingInt(ToIntFunction super T> mapper)

public static Collector averagingLong(ToLongFunction super T> mapper)

public static Collector averagingDouble(ToDoubleFunction super T> mapper)

上面这些方法比较简单,下面举个简单的例子介绍其使用:

129e9c87483b18adf5b82c26fcc80226

4 分组

Collectors提供了3个groupingBy重载方法,我们一个一个来理解。

4.1 从示例入手

我们从其中一个最简单的函数说起,从而慢慢引出

public static Collector>> groupingBy(

Function super T, ? extends K> classifier)

Collector>>

首先我们先来关注该方法的返回值Collector>,其最终返回的数据类型为:Map>

Function super T, ? extends K> classifier

分类函数。

示例如下:例如如下是购物车实体类,并且初始化数据如下:

public class ShopCar {

private int id;

private int sellerId;

private String sellerName;

private String goodsName;

private int buyerId;

private String buyerName;

private int num;

}

// 初始化数据如下:

public static List initShopCar() {

return Arrays.asList(

new ShopCar(1, 1, "天猫" , "华为手机", 1 , "dingw", 5),

new ShopCar(1, 2, "京东" , "华为手机", 2 , "ly", 2),

new ShopCar(1, 1, "京东" , "小米手机", 3 , "zhl", 3),

new ShopCar(1, 2, "1号店" , "华为手机", 1 , "dingw", 5),

new ShopCar(1, 2, "天猫" , "苹果手机", 1 , "dingw", 2)

);

}

首先我们看一下java8之前的写法:

public static void test_group_jdk7(List shopCars) {

Map> shopBySellerNameMap = new HashMap<>();

for(ShopCar c : shopCars ) {

if(shopBySellerNameMap.containsKey( c.getSellerName() )) {

shopBySellerNameMap.get(c.getSellerName()).add(c);

} else {

List aList = new ArrayList<>();

shopBySellerNameMap.put(c.getSellerName(), aList);

aList.add(c);

}

}

print(shopBySellerNameMap);

}

上面的代码应该很容易理解,根据商家名称进行分组,拥有相同商家的名称的购物车项组成一个集合,最终返回Map>类型的数据。

那如何使用java8的流分组特性来编写对应的代码呢?下面的思考过程非常关键,经过前面的学习,我想大家应该也具备了如下分析与编写的能力?

首先其声明如下:public static Collector>> groupingBy(Function super T, ? extends K> classifier),那在本例中,T,K这两个参数代表什么意思呢?

T : ShopCar

K : String (sellerName的类型)

其判断的主要依据为groupingBy方法返回的参数Collector>>,代表,其中最后一个泛型参数R对应的就是本例需要返回的Map>,故分析出T,K代表的含义。

然后再看其参数:Function super T, ? extends K> classifier,即接受的函数式编程接口为T -> K,即通过ShopCar 返回一个String,又根据其名称可知,该函数为一个分类函数,故基本可以写成如下代码:

public static void test_group_jdk8(List shopCars) {

Map> shopBySellerNameMap =

shopCars

.stream()

.collect(Collectors.groupingBy(ShopCar::getSellerName));

//.collect(Collectors.groupingBy( (ShopCar c) -> c.getSellerName() ))

print(shopBySellerNameMap);

}

其运行效果如下:

89b36f5c24476868127a4cd0cdfc8129

为了加深对groupingBy方法的理解,接下来我们重点分析一下其源码的实现。

4.2 源码分析groupingBy方法

public static Collector>> groupingBy(Function super T, ? extends K> classifier) { // @1

return groupingBy(classifier, toList()); // @2

}

代码@1:分类参数,已经在上文中详细介绍。

代码@2:调用groupingBy重载方法,传入的参数为toList(),有点意思,传入的参数为Collectors.toList(),结合上文中的示例,需要返回值类型为:Map>,与这里的List对应起来了。

public static Collector> groupingBy(Function super T, ? extends K> classifier, Collector super T, A, D> downstream) {

return groupingBy(classifier, HashMap::new, downstream);

}

该重载方法,再次调用3个参数的groupingBy方法,其中第二个参数为HashMap::new,即创建一个Map对象,我们重点关注3个参数的groupingBy。

public static > Collector groupingBy(

Function super T, ? extends K> classifier,

Supplier mapFactory,

Collector super T, A, D> downstream) { // @1

Supplier downstreamSupplier = downstream.supplier(); // @2 start

BiConsumer downstreamAccumulator = downstream.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);

}; // @2 end

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

@SuppressWarnings("unchecked")

Supplier> mangledFactory = (Supplier>) mapFactory;

if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) { // @4

return new CollectorImpl<>(mangledFactory, accumulator, merger, CH_ID);

}

else { // @5

@SuppressWarnings("unchecked")

Function downstreamFinisher = (Function) downstream.finisher();

Function, M> finisher = intermediate -> {

intermediate.replaceAll((k, v) -> downstreamFinisher.apply(v));

@SuppressWarnings("unchecked")

M castResult = (M) intermediate;

return castResult;

};

return new CollectorImpl<>(mangledFactory, accumulator, merger, finisher, CH_NOID);

}

}

代码@1:参数介绍:

Function super T, ? extends K> classifier

分类函数。

Supplier< M> mapFactory

map构建函数。()-> Map

Collector super T, A, D> downstream

下游收集器,在上面的示例中,该参数为Collectos.toList()。

代码@2:构建最终的累积器。其实现要点如下:

对流中的元素,使用Function super T, ? extends K> classifier,获取对应的分类键值。

使用mangledFactory创建累积初始值,并调用Map#computeIfAbsent方法,放入的值为:downstreamSupplier.get()。可以类比上例中Map>,请结合如下代码进行理解:

8da23363b7748cb6a8beac5355b195a3

代码@3:构建最终的组合器,这里使用的是Collectos.mapMerger,其内部的实现就是对每个元素,执行map#merge方法。

代码@4:如果收集器的行为为IDENTITY_FINISH,直接根据上面已创建的累积器、组合器,创建一个最终的收集器。

代码@5:如果收集器的行为不包含IDENTITY_FINISH,则需要最终调用原收集器的finisher方法。才能最终需要返回的类型。

groupingBy的原理就讲解到这里,我们接下来思考如下场景:

还是上面的购物车场景,现在要求先按照供应商名称分组,然后按照购买人分组(即多级分组),类似于SQL group by sellerId,buyerId。

思考过程:首先二级分类需要返回的数据类型为Map> >,而只有一个参数的groupingBy(Function super T, ? extends K> classifier),只接受一个分类参数,其内部会调用两个参数的groupingBy(Function super T, ? extends K> classifier,Collector super T, A, D> downstream),默认第二个参数为Collectors.toList(),故我们可以做的文章是改变这个默认值,传入符合业务场景的收集器,结合目前的需求,很显然,该参数应该是支持分组的收集器,即应该可以通过嵌套groupingBy方法,实现二级分组,其具体代码如下:

/**

* 二级分组示例

* @param shopCars

*/

public static void test_level_group(List shopCars) {

Map>> result =

shopCars.stream().collect(Collectors.groupingBy(ShopCar::getSellerName,

Collectors.groupingBy(ShopCar::getBuyerName)));

System.out.println(result);

}

温馨提示:上面介绍的分组,主要的Map存储结构为HashMap,java8为ConcurrentMap对应类继承体系提供了对应的分组函数:groupingByConcurrent,其使用方法与groupingBy方法类型,故不重复介绍。

5、 partitioningBy

分区,分区可以看出是分组的特殊化,接受的分类函数返回boolean类型,即是谓词Predicate super T> predicate。其声明如下:

public static Collector>> partitioningBy(Predicate super T> predicate)

public static Collector> partitioningBy(Predicate super T> predicate, Collector super T, A, D> downstream)

由于其用法与分组类似,故这里就一笔带过了。

6、 reducing

规约。其函数声明如下:

public static Collector reducing(U identity, Function super T, ? extends U> mapper, BinaryOperatorop)

其参数如下:

U identity

规约初始值。

Function super T, ? extends U> mapper

累加器函数。

BinaryOperatorop

组合器函数。

关于Collectors.reducing,建议可以直接使用Stream自身提供的reducing方法,具体请参考博文:java8实战读书笔记:初识Stream、流的基本操作(流计算)


版权声明:本文为weixin_36293680原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。