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

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

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


image.png

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

1public static <T> Collector<T, ?, List<T>> toList()
2public static <T> Collector<T, ?, Set<T>> toSet()
3public static <T, C extends Collection<T>> Collector<T, ?, C> toCollection(Supplier<C> collectionFactory)

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


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


1public static void test_toList(List<Dish> menu) {
2    List<String> names = menu.stream().map(Dish::getName)
3                        .collect(Collectors.toList());
4}

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

image.png

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

1public static Collector<CharSequence, ?, String> joining()
2public static Collector<CharSequence, ?, String> joining(CharSequence delimiter)
3public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
4    CharSequence prefix, CharSequence suffix)


2.1 joining


1public static Collector<CharSequence, ?, String> joining() {
2    return new CollectorImpl<CharSequence, StringBuilder, String>(
3        StringBuilder::new, StringBuilder::append,
4        (r1, r2) -> { r1.append(r2); return r1; },
5        StringBuilder::toString, CH_NOID);
6}
  • Supplier< A> supplier()
    其函数为StringBuilder::new,即通过该方法创建一个StringBuilder方法,作为累积器的初始值。
  • BiConsumer accumulator
    累积器:StringBuilder::append,即会对流中的元素执行追加。
  • BinaryOperator< A> combiner
    组合器,也是调用append方法,进行字符串的规约。
  • Function finisher
    转换器:由于累积器返回的最终对象为StringBuilder,并不是目标String类型,故需要调用StringBuilder#toString方法进行转换
  • Set< Characteristics> characteristics
    无任何行为。


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

830d7ab4cef2802aa8100b090bf8b958.jpg



2.2 joining(CharSequence delimiter)


1public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
 2    return joining(delimiter, "", "");
 3}
 4public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
 5                                                         CharSequence prefix,
 6                                                         CharSequence suffix) {
 7    return new CollectorImpl<>(
 8            () -> new StringJoiner(delimiter, prefix, suffix),
 9            StringJoiner::add, StringJoiner::merge,
10            StringJoiner::toString, CH_NOID);
11}
  • 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
    无任何行为。


其示例如下:

dd821379377eb44960f3850c78c4ec1f.jpg

image.png

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

1public static <T> Collector<T, ?, Optional<T>> minBy(Comparator<? super T> comparator)
2public static <T> Collector<T, ?, Optional<T>> maxBy(Comparator<? super T> comparator)
3public static <T> Collector<T, ?, Integer> summingInt(ToIntFunction<? super T> mapper)
4public static <T> Collector<T, ?, Long> summingLong(ToLongFunction<? super T> mapper)
5public static <T> Collector<T, ?, Double> summingDouble(ToDoubleFunction<? super T> mapper)
6public static <T> Collector<T, ?, Double> averagingInt(ToIntFunction<? super T> mapper)
7public static <T> Collector<T, ?, Double> averagingLong(ToLongFunction<? super T> mapper)
8public static <T> Collector<T, ?, Double> averagingDouble(ToDoubleFunction<? super T> mapper)

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

84f4d842b4be2a4b9f1bbac8be8492ca.jpg

image.png

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


4.1 从示例入手


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

1public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(
2             Function<? super T, ? extends K> classifier)
  • Collector>>
    首先我们先来关注该方法的返回值Collector<T, ?, Map<K,List< T>>,其最终返回的数据类型为:Map<K, List< T >>
  • Function classifier
    分类函数。


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


1public class ShopCar {
 2    private int id;
 3    private int sellerId;
 4    private String sellerName;
 5    private String goodsName;
 6    private int buyerId;
 7    private String buyerName;
 8    private int num;
 9}
10// 初始化数据如下:
11public static List<ShopCar> initShopCar() {
12    return Arrays.asList(
13            new ShopCar(1, 1, "天猫" , "华为手机", 1 , "dingw", 5),
14            new ShopCar(1, 2, "京东" , "华为手机", 2 , "ly", 2),
15            new ShopCar(1, 1, "京东" , "小米手机", 3 , "zhl", 3),
16            new ShopCar(1, 2, "1号店" , "华为手机", 1 , "dingw", 5),
17            new ShopCar(1, 2, "天猫" , "苹果手机", 1 , "dingw", 2)
18    );
19}

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

1public static void test_group_jdk7(List<ShopCar> shopCars) {
 2    Map<String, List<ShopCar>> shopBySellerNameMap = new HashMap<>();
 3    for(ShopCar c : shopCars ) {
 4        if(shopBySellerNameMap.containsKey( c.getSellerName() )) {
 5            shopBySellerNameMap.get(c.getSellerName()).add(c);
 6        } else {
 7            List<ShopCar> aList = new ArrayList<>();
 8            shopBySellerNameMap.put(c.getSellerName(), aList);
 9            aList.add(c);
10        }
11    }
12    print(shopBySellerNameMap);
13}

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


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


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


  • T : ShopCar
  • K : String (sellerName的类型)
    其判断的主要依据为groupingBy方法返回的参数Collector< T, ?, Map< K, List< T>>>,代表< T, A, R>,其中最后一个泛型参数R对应的就是本例需要返回的Map< K, List< T>>,故分析出T,K代表的含义。


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


1public static void test_group_jdk8(List<ShopCar> shopCars) {
2    Map<String, List<ShopCar>> shopBySellerNameMap =  
3                 shopCars
4                     .stream()
5                     .collect(Collectors.groupingBy(ShopCar::getSellerName));
6                   //.collect(Collectors.groupingBy( (ShopCar c) -> c.getSellerName() ))
7    print(shopBySellerNameMap);
8}

其运行效果如下:

fe97f173dc58608deed1b83e726d6fbd.png

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


4.2 源码分析groupingBy方法


1public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier) {  // @1
2    return groupingBy(classifier, toList());                                                                     // @2
3}

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


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


1public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) {
2    return groupingBy(classifier, HashMap::new, downstream);
3}

该重载方法,再次调用3个参数的groupingBy方法。

1public static <T, K, D, A, M extends Map<K, D>> Collector<T, ?, M> groupingBy(
 2                          Function<? super T, ? extends K> classifier, 
 3Supplier<M> mapFactory,
 4Collector<? super T, A, D> downstream) { // @1
 5    Supplier<A> downstreamSupplier = downstream.supplier();        // @2 start
 6    BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
 7    BiConsumer<Map<K, A>, T> accumulator = (m, t) -> {
 8        K key = Objects.requireNonNull(classifier.apply(t), "element cannot be mapped to a null key");
 9        A container = m.computeIfAbsent(key, k -> downstreamSupplier.get());
10        downstreamAccumulator.accept(container, t);
11    }; // @2 end
12
13    BinaryOperator<Map<K, A>> merger = Collectors.<K, A, Map<K, A>>mapMerger(downstream.combiner());   // @3
14    @SuppressWarnings("unchecked")
15    Supplier<Map<K, A>> mangledFactory = (Supplier<Map<K, A>>) mapFactory;                            
16
17    if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {           // @4
18        return new CollectorImpl<>(mangledFactory, accumulator, merger, CH_ID);
19    }
20    else {                                                                                            // @5
21        @SuppressWarnings("unchecked")
22        Function<A, A> downstreamFinisher = (Function<A, A>) downstream.finisher();
23        Function<Map<K, A>, M> finisher = intermediate -> {
24            intermediate.replaceAll((k, v) -> downstreamFinisher.apply(v));
25            @SuppressWarnings("unchecked")
26            M castResult = (M) intermediate;
27            return castResult;
28        };
29        return new CollectorImpl<>(mangledFactory, accumulator, merger, finisher, CH_NOID);
30    }
31}

代码@1:参数介绍:


  • Function classifier
    分类函数。
  • Supplier< M> mapFactory
    map构建函数。()-> Map
  • Collector downstream
    下游收集器,在上面的示例中,该参数为Collectos.toList()。


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


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

   d95e13cadbb18d3cdbe0d4e17002a455.jpg

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


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


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


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


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


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


1/**
 2 * 二级分组示例
 3 * @param shopCars
 4 */
 5public static void test_level_group(List<ShopCar> shopCars) {
 6    Map<String, Map<String, List<ShopCar>>>  result = 
 7        shopCars.stream().collect(Collectors.groupingBy(ShopCar::getSellerName,
 8                                    Collectors.groupingBy(ShopCar::getBuyerName)));
 9    System.out.println(result);
10}

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

image.png

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

1public static <T> Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate)
2public static <T, D, A> Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T, A, D> downstream)

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

image.png

规约。其函数声明如下:

1public static <T, U> Collector<T, ?, U> reducing(U identity, Function<? super T, ? extends U> mapper, BinaryOperator<U> op)

其参数如下:


相关文章
|
16天前
|
前端开发 JavaScript Java
java常用数据判空、比较和类型转换
本文介绍了Java开发中常见的数据处理技巧,包括数据判空、数据比较和类型转换。详细讲解了字符串、Integer、对象、List、Map、Set及数组的判空方法,推荐使用工具类如StringUtils、Objects等。同时,讨论了基本数据类型与引用数据类型的比较方法,以及自动类型转换和强制类型转换的规则。最后,提供了数值类型与字符串互相转换的具体示例。
|
3天前
|
Java
Java基础却常被忽略:全面讲解this的实战技巧!
本次分享来自于一道Java基础的面试试题,对this的各种妙用进行了深度讲解,并分析了一些关于this的常见面试陷阱,主要包括以下几方面内容: 1.什么是this 2.this的场景化使用案例 3.关于this的误区 4.总结与练习
|
19天前
|
Java 程序员
Java基础却常被忽略:全面讲解this的实战技巧!
小米,29岁程序员,分享Java中`this`关键字的用法。`this`代表当前对象引用,用于区分成员变量与局部变量、构造方法间调用、支持链式调用及作为参数传递。文章还探讨了`this`在静态方法和匿名内部类中的使用误区,并提供了练习题。
20 1
|
23天前
|
JSON Java 程序员
Java|如何用一个统一结构接收成员名称不固定的数据
本文介绍了一种 Java 中如何用一个统一结构接收成员名称不固定的数据的方法。
25 3
|
1月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
52 6
|
29天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
1月前
|
Java 程序员 容器
Java中的变量和常量:数据的‘小盒子’和‘铁盒子’有啥不一样?
在Java中,变量是一个可以随时改变的数据容器,类似于一个可以反复打开的小盒子。定义变量时需指定数据类型和名称。例如:`int age = 25;` 表示定义一个整数类型的变量 `age`,初始值为25。 常量则是不可改变的数据容器,类似于一个锁死的铁盒子,定义时使用 `final` 关键字。例如:`final int MAX_SPEED = 120;` 表示定义一个名为 `MAX_SPEED` 的常量,值为120,且不能修改。 变量和常量的主要区别在于变量的数据可以随时修改,而常量的数据一旦确定就不能改变。常量主要用于防止意外修改、提高代码可读性和便于维护。
|
1月前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
71 2
|
SQL Java 关系型数据库
java8 多字段分组+count
java8 多字段分组+count
java8 多字段分组+count
|
7天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
37 6
下一篇
DataWorks