图解Stream之collect:长文深度分析让你彻底掌握流式编程

开发 前端
collect 操作是 Stream 流处理中的关键一步,用于将处理后的元素以指定的方式进行收集和汇总。下面我们对collect相关的操作原理及方法进行详细地介绍,确保我们完全掌握collect的使用。

在 Java 8 中,引入了 Stream 流的概念,它是对集合数据进行操作的一种高级抽象。Stream 具有以下几个主要特点和优势:

  1. 声明式编程 通过简洁的方式表达对数据的处理逻辑,而无需关注具体的实现细节。例如,使用 filter 方法筛选出符合条件的元素,使用 map 方法对元素进行转换。
  2. 懒加载Stream 的操作并非立即执行,而是在终端操作(如 collect、forEach 等)被调用时才真正执行。这有助于提高性能,避免不必要的计算。
  3. 链式操作 可以将多个操作连接在一起,形成一个连贯的处理流程,使代码更具可读性和可维护性。
  4. 并行处理 可以方便地实现并行计算,充分利用多核 CPU 的优势,提高处理大规模数据的效率。

而在 Stream 流中,collect 操作是一个终端操作,用于将 Stream 中的元素收集到一个新的集合或数据结构中。Stream 提供了对数据的一系列中间操作,如 filter、map、sorted 等,这些操作只是定义了对数据的处理逻辑,但不会真正执行对数据的处理。而 collect 操作作为终端操作,触发之前定义的中间操作的执行,并将处理后的结果进行收集。

总之,collect 操作是 Stream 流处理中的关键一步,用于将处理后的元素以指定的方式进行收集和汇总。下面我们对collect相关的操作原理及方法进行详细地介绍,确保我们完全掌握collect的使用。

Collectors介绍

我们先看看Collect、Collector和Collectors的区别:

  • collect 是 Java 8 中 Stream 流的一个方法,用于对流中的元素进行收集操作。它需要传入一个实现了 Collector 接口的收集器来指定具体的收集行为。
  • Collector 是一个接口,定义了收集流元素的规范和方法。通过实现 Collector 接口,可以自定义收集器来实现特定的元素收集逻辑。
  • Collectors 是一个工具类,它提供了许多静态方法,用于方便地创建常见的 Collector 实现。这些预定义的收集器可以满足大多数常见的收集需求,例如将流元素收集到列表、集合、映射等,或者进行分组、分区、规约汇总等操作。

例如,使用 Collectors.toList() 可以创建一个将流元素收集到列表的收集器,然后将其传递给 collect 方法,对流进行收集操作并得到一个包含所有元素的列表。

图片图片

概括来说:

  • collect 是 Stream 流的终止方法,使用传入的收集器(必须是 Collector 接口的某个具体实现类)对结果执行相关操作。
  • Collector 是一个接口,collect 方法接收的收集器是 Collector 接口的具体实现类。
  • Collectors 是一个工具类,提供了很多静态工厂方法,用于创建各种预定义的 Collector 接口的具体实现类,方便程序员使用。如果不使用 Collectors 类,自己去实现 Collector 接口也是可以的。

图片图片

Collectors的方法

图片图片

恒等处理

指的就是Stream的元素在经过Collector函数处理前后完全不变,例如toList()操作,只是最终将结果从Stream中取出放入到List对象中,并没有对元素本身做任何的更改处理。

图片图片

归约汇总

Stream流中的元素被逐个遍历,进入到Collector处理函数中,然后会与上一个元素的处理结果进行合并处理,并得到一个新的结果,以此类推,直到遍历完成后,输出最终的结果。

图片图片

分组分区

Collectors工具类中提供了groupingBy和partitioningBy方法进行数据分区,区别在于partitioningBy仅基于条件分成两个组。

图片图片

Collector的原理

要自定义收集器Collector,需要实现Collector接口中定义的五个方法,分别是:supplier()、accumulator()、combiner()、finisher()和characteristics()。

图片图片

这5个方法的含义说明归纳如下:

接口名称

功能含义说明

supplier

创建新的结果容器,可以是一个容器,也可以是一个累加器实例,总之是用来存储结果数据的

accumlator

元素进入收集器中的具体处理操作

finisher

当所有元素都处理完成后,在返回结果前的对结果的最终处理操作,当然也可以选择不做任何处理,直接返回

combiner

各个子流的处理结果最终如何合并到一起去,比如并行流处理场景,元素会被切分为好多个分片进行并行处理,最终各个分片的数据需要合并为一个整体结果,即通过此方法来指定子结果的合并逻辑

characteristics

对此收集器处理行为的补充描述,比如此收集器是否允许并行流中处理,是否finisher方法必须要有等等,此处返回一个Set集合,里面的候选值是固定的几个可选项。

对于characteristics返回set集合中的可选值,说明如下:

取值

含义说明

UNORDERED

无序。声明此收集器的汇总归约结果与Stream流元素遍历顺序无关,不受元素处理顺序影响

CONCURRENT

并行。声明此收集器可以多个线程并行处理,允许并行流中进行处理

IDENTITY_FINISH

恒等映射。声明此收集器的finisher方法是一个恒等操作

现在,我们知道了这5个接口方法各自的含义与用途了,那么作为一个Collector收集器,这几个接口之间是如何配合处理并将Stream数据收集为需要的输出结果的呢?下面这张图可以清晰的阐述这一过程:

图片图片

如果我们的Collector是支持在并行流中使用的,则其处理过程有所不同:

图片图片

下面的例子展示如何自定义一个将元素收集到LinkedList的收集器:

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

public class MyCollector implements Collector<String, List<String>, List<String>> {

    // supplier()方法返回一个Supplier,它创建了一个空的LinkedList实例,作为收集数据的容器。
    @Override
    public Supplier<List<String>> supplier() {
        return LinkedList::new; 
    }

    // accumulator()方法返回一个BiConsumer,用于将流中的元素添加到LinkedList中。
    @Override
    public BiConsumer<List<String>, String> accumulator() {
        return List::add; 
    }

    // combiner()方法返回一个BinaryOperator,用于合并多个LinkedList。当流被并行处理时,可能会有多个子部分的结果需要合并,这里将两个LinkedList合并为一个。
    @Override
    public BinaryOperator<List<String>> combiner() {
        return (r1, r2) -> {
            r1.addAll(r2);
            return r1;
        };
    }

    // finisher()方法返回一个Function,在遍历完流后,将累加器对象(在这里就是LinkedList本身)转换为最终结果。在这个例子中,累加器对象就是最终结果,所以直接返回它。
    @Override
    public Function<List<String>, List<String>> finisher() {
        return list -> list; 
    }

    // characteristics()方法返回一个包含收集器特征的EnumSet。这里使用了IDENTITY_FINISH特征,表示finisher方法返回的是一个恒等函数,可以跳过,直接将累加器作为最终结果。
    @Override
    public EnumSet<Collector.Characteristics> characteristics() {
        return EnumSet.of(Collector.Characteristics.IDENTITY_FINISH); 
    }
}

下面我们用自定义的收集器进行处理:

List<String> input = Arrays.asList("apple", "banana", "orange");
List<String> result = input.stream().collect(new MyCollector());

如果希望收集器具有其他特性,例如支持并行处理(CONCURRENT)、不保证元素顺序(UNORDERED)等,可以在characteristics()方法中添加相应的特性。例如,如果你的收集器支持并行处理且不保证元素顺序,可以这样返回特性集合:

return EnumSet.of(Collector.Characteristics.CONCURRENT, Collector.Characteristics.UNORDERED);

另外,还可以根据具体的需求自定义收集器的逻辑,例如过滤元素、执行特定的计算等。

Collectors方法深究

groupingBy分组

Collectors.groupingBy是 Java 8 中Stream API 的一个收集器,用于将流中的元素根据某个分类函数收集到Map中。

groupingBy的构造方法

  • groupingBy(Function):基本的分组,默认使用List收集,

图片图片

相当于groupingBy(classifier, toList())。我们用下面的代码实现,对学生按照年龄段进行分组:

Map<Integer, List<Student>> nameListByAge = students.stream().collect(Collectors.groupingBy(Student::getAge));
  • groupingBy(Function, Collector):可指定收集器的分组

图片图片

这里使用Set集合收集。

// 不同年龄段的学生集合,去重
Map<Integer, Set<String>> namesByAge = students.stream().collect(Collectors.groupingBy(
        Student::getAge,
        Collectors.mapping(Student::getName, Collectors.toSet()))
);
  • groupingBy(Function, Supplier, Collector):可指定存储容器和收集器的分组

图片图片

下面使用TreeMap作为容器,保证了键的有序性。但是分组之后的组内数据不是有序的。

// 【键有序】不同年龄段的学生集合,去重,年龄按照升序排列
Map<Integer, Set<String>> namesBySortedAge = students.stream().collect(Collectors.groupingBy(
    Student::getAge,
    TreeMap::new,
    Collectors.mapping(Student::getName, Collectors.toSet()))
);

如果要保证分组之后的数据有序,有下面两种方法:

  • collectingAndThen:先分组,再使用collectingAndThen聚合操作,对组内数据进行排序。
Map<Integer, List<Student>> sortedCollect = students.stream()
            .collect(Collectors.groupingBy(
                    Student::getAge,
                    Collectors.collectingAndThen(
                            // 先收集到List
                            Collectors.toList(),
                            // 然后对每个List进行排序
                            list -> list.stream().sorted(Comparator.comparing(Student::getScore)).collect(Collectors.toList())
                    )
            ));
  • mapping:使用第二种构造方法,对组内元素收集到list,然后使用TreeSet集合进行收集。
// 按照年龄分组,组内按照分数升序
Map<Integer, TreeSet<Student>> collect = students.stream().collect(Collectors.groupingBy(
        Student::getAge,
        Collectors.mapping(student -> student, Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Student::getScore))))
        )
);

基础分组功能

  • 按照对象的某个字段进行分组:假设有一个学生类Student,包含course(课程)字段,可以按照课程对学生进行分组。
Map<String, List<Student>> groupByCourse = students.stream()
  .collect(Collectors.groupingBy(Student::getCourse));
  • 自定义键的映射:根据学生对象的多个字段或进行某种格式化操作来生成键。
Map<String, List<Student>> groupByCustomKey = students.stream()
  .collect(Collectors.groupingBy(student -> student.getName() + "_" + student.getAge()));
  • 自定义容器类型:如使用LinkedHashMap保证分组后键的有序性。
Map<String, List<Student>> groupByCourseWithLinkedHashMap = students.stream()
 .collect(Collectors.groupingBy(Student::getCourse, LinkedHashMap::new, Collectors.toList()));

分组统计功能

  • 计数:计算每个分组中的元素数量。
Map<String, Long> courseCountMap = students.stream()
  .collect(Collectors.groupingBy(Student::getCourse, Collectors.counting()));
  • 求和:对每个分组中的某个数值字段进行求和。
Map<String, Integer> totalScoreByCourseMap = students.stream()
  .collect(Collectors.groupingBy(Student::getCourse, Collectors.summingInt(Student::getScore)));
  • 平均值:计算每个分组中某个数值字段的平均值。
Map<String, Double> averageScoreByCourseMap = students.stream()
  .collect(Collectors.groupingBy(Student::getCourse, Collectors.averagingInt(Student::getScore)));
  • 最大最小值:获取每个分组中某个数值字段的最大值或最小值。
Map<String, Student> maxScoreStudentByCourseMap = students.stream()
 .collect(Collectors.groupingBy(Student::getCourse, Collectors.maxBy(Comparator.comparingInt(Student::getScore))));

Map<String, Student> minScoreStudentByCourseMap = students.stream()
 .collect(Collectors.groupingBy(Student::getCourse, Collectors.minBy(Comparator.comparingInt(Student::getScore))));
  • 完整统计:同时获取计数、总和、平均值、最大最小值等统计结果。
Map<String, IntSummaryStatistics> summaryStatisticsByCourseMap = students.stream()
 .collect(Collectors.groupingBy(Student::getCourse, Collectors.summarizingInt(Student::getScore)));
  • 范围统计:根据某个条件进行范围分组统计。
Map<Boolean, List<Student>> dividedByScore = students.stream()
 .collect(Collectors.partitioningBy(student -> student.getScore() >= 60));

分组合并功能

合并分组结果:使用reducing方法对每个分组的元素进行自定义的合并操作。

Map<String, String> combinedNamesByCourseMap = students.stream()
 .collect(Collectors.groupingBy(Student::getCourse, Collectors.reducing("", Student::getName, (name1, name2) -> name1 + ", " + name2)));

合并字符串:将每个分组中的字符串元素连接起来。

Map<String, String> joinedNamesByCourseMap = students.stream()
 .collect(Collectors.groupingBy(Student::getCourse, Collectors.joining(", ")));

分组自定义映射功能

映射结果为Collection对象:将每个分组的元素映射为另一个Collection对象。

Map<String, Set<Student>> studentsSetByCourseMap = students.stream()
 .collect(Collectors.groupingBy(Student::getCourse, Collectors.toSet()));

自定义映射结果:通过mapping方法进行更复杂的映射操作。

Map<String, List<String>> studentNamesByCourseMap = students.stream()
 .collect(Collectors.groupingBy(Student::getCourse, Collectors.mapping(Student::getName, Collectors.toList())));

自定义downstream收集器:更灵活地控制分组后的值的收集方式。

Collector<Student,?, Map<String, CustomResult>> customCollector = Collector.of(
    HashMap::new, 
    (map, student) -> { 
        // 自定义的收集逻辑,将学生对象转换为 CustomResult 并添加到 map 中 
    },
    (map1, map2) -> { 
        // 合并两个 map 的逻辑 
    });

Map<String, CustomResult> customResultMap = students.stream()
 .collect(Collectors.groupingBy(Student::getCourse, customCollector));

多级分组可以通过嵌套使用groupingBy来实现。例如,假设有一个包含学生信息的列表,要先按班级分组,然后在每个班级内再按性别分组,可以这样写:

Map<String, Map<String, List<Student>>> groupedByClassAndGender = students.stream()
  .collect(Collectors.groupingBy(Student::getClass, Collectors.groupingBy(Student::getGender)));

在上述示例中,外层的groupingBy按照班级进行分组,得到的每个班级的分组结果(本身也是一个Map)又通过内层的groupingBy按照性别进一步分组。这样最终得到的是一个两级分组的Map结构。

partitioningBy分类

掌握了groupingBy,现在看partitioningBy就简单很多了。就两个简单的构造方法:

// 仅提供分类器
partitioningBy(Predicate<? super T> predicate) 

// 提供分类器和下游收集器
partitioningBy(Predicate<? super T> predicate,Collector<? super T, A, D> downstream)

比如我们筛选成年人和非成年人:

Map<Boolean, List<Student>> adultList = students.stream().collect(Collectors.partitioningBy(s -> s.getAge() > 18));

Map<Boolean, Set<Student>> adultSet = students.stream().collect(Collectors.partitioningBy(s -> s.getAge() > 18, Collectors.toSet()));

结果如下图所示:

图片图片

collectingAndThen分组处理

图片图片

从方法签名可以看出,需要传入一个收集器和一个处理函数,相当于收集了数据之后,再进行后续操作。如下图所示:

图片图片

比如,前面提到的,先分组,再排序:

Map<Integer, List<Student>> sortedCollect = students.stream()
            .collect(Collectors.groupingBy(
                    Student::getAge,
                    Collectors.collectingAndThen(
                            // 先收集到List
                            Collectors.toList(),
                            // 然后对每个List进行排序
                            list -> list.stream().sorted(Comparator.comparing(Student::getScore)).collect(Collectors.toList())
                    )
            ));

reducing归集操作

单参数:输入归集操作

  • BinaryOperator accumulator 归集操作函数 输入参数T返回T

图片图片

比如实现数组的内容求和:

List<Integer> testData = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Optional<Integer> sum = testData.stream().collect(Collectors.reducing((prev, cur) -> {
    System.out.println("prev=>" + prev + "cur=>" + cur);
    return prev + cur;
}));
System.out.print(sum.get()); // 45

双参数:输入初始值、归集操作 参数说明

  • T identity 返回类型T初始值
  • BinaryOperator accumulator 归集操作函数 输入参数T返回T

下面是增加了初始值的求和操作:

List<Integer> testData = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Integer sum = testData.stream().collect(Collectors.reducing(20, (prev, cur) -> {
    System.out.println("prev=>" + prev + "cur=>" + cur);
    return prev + cur;
}));
System.out.print(sum); //65

三参数:这个函数才是真正体现reducing(归集)的过程。调用者要明确知道以下三点

  1. 需要转换类型的初始值
  2. 类型如何转换
  3. 如何收集返回值

参数说明

  • U identity 最终返回类型U初始值
  • BiFunction<U, ? super T, U> accumulator, 将输入参数T转换成返回类型U的函数
  • BinaryOperator  combiner 归集操作函数 输入参数U返回U

图片图片

比如实现单数字转字符串并按逗号连接的功能:

List<Integer> testData = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
String joinStr = testData.stream().collect(Collectors.reducing("转换成字符串", in -> {
    return in + "";
}, (perv, cur) -> {
    return perv + "," + cur;
}));
System.out.print(joinStr); // 转换成字符串,1,2,3,4,5,6,7,8,9

责任编辑:武晓燕 来源: 松语编程
相关推荐

2021-04-15 07:32:02

java 代码Stream

2021-08-11 22:17:48

负载均衡LVS机制

2023-07-06 08:31:50

Python对象编程

2020-12-08 08:14:11

SQL注入数据库

2019-09-24 08:16:14

Reactor响应式编程

2019-07-11 14:45:52

简历编程项目

2016-03-28 09:39:54

2009-11-06 09:39:40

WCF契约

2024-03-15 08:23:26

异步编程函数

2019-02-25 09:20:53

2014-11-05 10:58:00

编程

2024-06-21 09:27:05

2024-04-12 09:01:08

2021-08-12 18:48:31

响应式编程Bio

2023-05-29 08:11:42

@Value注解Bean

2020-11-03 10:32:48

回调函数模块

2017-06-07 18:40:33

PromiseJavascript前端

2024-01-17 08:18:14

RPAJava技术

2022-09-16 08:32:17

Reduxreact

2018-05-21 10:20:22

人工智能机器学习神经网络
点赞
收藏

51CTO技术栈公众号