在 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的收集器:
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(归集)的过程。调用者要明确知道以下三点
- 需要转换类型的初始值
- 类型如何转换
- 如何收集返回值
参数说明
- 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