在 Java 8 中,引入了 Stream 流的概念,它是对集合数据进行操作的一种高级抽象。Stream 具有以下几个主要特点和优势:
- 声明式编程 通过简洁的方式表达对数据的处理逻辑,而无需关注具体的实现细节。例如,使用 filter 方法筛选出符合条件的元素,使用 map 方法对元素进行转换。
- 懒加载Stream 的操作并非立即执行,而是在终端操作(如 collect、forEach 等)被调用时才真正执行。这有助于提高性能,避免不必要的计算。
- 链式操作 可以将多个操作连接在一起,形成一个连贯的处理流程,使代码更具可读性和可维护性。
- 并行处理 可以方便地实现并行计算,充分利用多核 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的收集器:
下面我们用自定义的收集器进行处理:
如果希望收集器具有其他特性,例如支持并行处理(CONCURRENT)、不保证元素顺序(UNORDERED)等,可以在characteristics()方法中添加相应的特性。例如,如果你的收集器支持并行处理且不保证元素顺序,可以这样返回特性集合:
另外,还可以根据具体的需求自定义收集器的逻辑,例如过滤元素、执行特定的计算等。
Collectors方法深究
groupingBy分组
Collectors.groupingBy是 Java 8 中Stream API 的一个收集器,用于将流中的元素根据某个分类函数收集到Map中。
groupingBy的构造方法
- groupingBy(Function):基本的分组,默认使用List收集,
图片
相当于groupingBy(classifier, toList())。我们用下面的代码实现,对学生按照年龄段进行分组:
- groupingBy(Function, Collector):可指定收集器的分组
图片
这里使用Set集合收集。
- groupingBy(Function, Supplier, Collector):可指定存储容器和收集器的分组
图片
下面使用TreeMap作为容器,保证了键的有序性。但是分组之后的组内数据不是有序的。
如果要保证分组之后的数据有序,有下面两种方法:
- collectingAndThen:先分组,再使用collectingAndThen聚合操作,对组内数据进行排序。
- mapping:使用第二种构造方法,对组内元素收集到list,然后使用TreeSet集合进行收集。
基础分组功能
- 按照对象的某个字段进行分组:假设有一个学生类Student,包含course(课程)字段,可以按照课程对学生进行分组。
- 自定义键的映射:根据学生对象的多个字段或进行某种格式化操作来生成键。
- 自定义容器类型:如使用LinkedHashMap保证分组后键的有序性。
分组统计功能
- 计数:计算每个分组中的元素数量。
- 求和:对每个分组中的某个数值字段进行求和。
- 平均值:计算每个分组中某个数值字段的平均值。
- 最大最小值:获取每个分组中某个数值字段的最大值或最小值。
- 完整统计:同时获取计数、总和、平均值、最大最小值等统计结果。
- 范围统计:根据某个条件进行范围分组统计。
分组合并功能
合并分组结果:使用reducing方法对每个分组的元素进行自定义的合并操作。
合并字符串:将每个分组中的字符串元素连接起来。
分组自定义映射功能
映射结果为Collection对象:将每个分组的元素映射为另一个Collection对象。
自定义映射结果:通过mapping方法进行更复杂的映射操作。
自定义downstream收集器:更灵活地控制分组后的值的收集方式。
多级分组可以通过嵌套使用groupingBy来实现。例如,假设有一个包含学生信息的列表,要先按班级分组,然后在每个班级内再按性别分组,可以这样写:
在上述示例中,外层的groupingBy按照班级进行分组,得到的每个班级的分组结果(本身也是一个Map)又通过内层的groupingBy按照性别进一步分组。这样最终得到的是一个两级分组的Map结构。
partitioningBy分类
掌握了groupingBy,现在看partitioningBy就简单很多了。就两个简单的构造方法:
比如我们筛选成年人和非成年人:
结果如下图所示:
图片
collectingAndThen分组处理
图片
从方法签名可以看出,需要传入一个收集器和一个处理函数,相当于收集了数据之后,再进行后续操作。如下图所示:
图片
比如,前面提到的,先分组,再排序:
reducing归集操作
单参数:输入归集操作
- BinaryOperator accumulator 归集操作函数 输入参数T返回T
图片
比如实现数组的内容求和:
双参数:输入初始值、归集操作 参数说明
- T identity 返回类型T初始值
- BinaryOperator accumulator 归集操作函数 输入参数T返回T
下面是增加了初始值的求和操作:
三参数:这个函数才是真正体现reducing(归集)的过程。调用者要明确知道以下三点
- 需要转换类型的初始值
- 类型如何转换
- 如何收集返回值
参数说明
- U identity 最终返回类型U初始值
- BiFunction<U, ? super T, U> accumulator, 将输入参数T转换成返回类型U的函数
- BinaryOperator combiner 归集操作函数 输入参数U返回U
图片
比如实现单数字转字符串并按逗号连接的功能: