
本文详细介绍了如何利用java stream api,特别是collectors.groupingby与collectors.reducing组合,高效地对数据进行多条件聚合。通过构建自定义度量类,我们能够同时实现按月份对特定数值进行求和,并统计对应事件的数量。教程涵盖了数据模型定义、聚合逻辑实现以及最终结果的映射转换,并强调了在计数场景中事件计数与去重实体计数的区别。
在现代Java应用中,对集合数据进行高效的统计、分组和聚合是常见的需求。Java Stream API提供了强大而灵活的工具来处理这类任务。本教程将深入探讨如何使用Collectors.groupingBy结合Collectors.reducing来实现复杂的数据聚合,例如按月份分组,并同时计算某个值的总和以及组内元素的数量。
假设我们有一个Person对象的列表,每个Person对象包含ID、事件类型、事件日期和关联值。我们的目标是:
原始数据示例如下:
ID,Info,Date,Value per1, STATUS1, 10-01-2022, 1 per2, STATUS2, 10-01-2022, 2 per3, STATUS3, 10-01-2022, 3 per1, STATUS4, 10-01-2022, 1 per1, STATUS1, 10-02-2022, 1 per2, STATUS1, 10-03-2022, 1 per3, STATUS2, 10-03-2022, 2
我们期望的输出结果格式为:
立即学习“Java免费学习笔记(深入)”;
Month | Total Sum | Person Count (事件数) 1 7 4 2 1 1 3 3 2
请注意,这里Person Count指的是每个月发生的Person事件记录数,而不是去重后的Person ID数量。
首先,我们需要定义Person类和相关的枚举类型。为了简化示例,我们将Person的value字段定义为int类型,并使用Java 14引入的record特性来创建简洁的数据类。
import java.time.LocalDate;
// 事件类型枚举
enum Statement {
STATUS1, STATUS2, STATUS3, STATUS4 // 扩展以包含示例数据中的所有状态
}
// Person记录类,包含ID、事件类型、事件日期和值
record Person(String id,
Statement event,
LocalDate eventDate,
int value) {}
// 最终结果的DTO
record DTO(int month, int totalSum, int totalPersons) {} // totalPersons在此表示事件计数为了在一次Stream操作中同时计算总和和计数,我们创建一个PersonGroupMetric记录类来封装这些聚合结果。这个类将作为Collectors.reducing操作的中间累加器。
record PersonGroupMetric(int count, int sum) {
// 定义一个空的度量实例,作为reducing操作的初始值
public static final PersonGroupMetric EMPTY = new PersonGroupMetric(0, 0);
// 构造函数:将单个Person对象映射为初始的PersonGroupMetric
// 每个Person对象对应一个事件,所以count为1,sum为该Person的值
public PersonGroupMetric(Person p) {
this(1, p.value());
}
// 合并方法:将两个PersonGroupMetric实例合并
// 累加它们的count和sum
public PersonGroupMetric add(PersonGroupMetric other) {
return new PersonGroupMetric(
this.count + other.count,
this.sum + other.sum
);
}
}PersonGroupMetric的设计是关键:
有了数据模型和度量类,我们现在可以使用Collectors.groupingBy和Collectors.reducing来执行聚合操作。
首先,准备示例数据:
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.reducing;
import static java.util.stream.Collectors.toList;
public class StreamAggregationTutorial {
public static void main(String[] args) {
var src = List.of(
new Person("per1", Statement.STATUS1, LocalDate.of(2022, 01, 10), 1),
new Person("per2", Statement.STATUS2, LocalDate.of(2022, 01, 10), 2),
new Person("per3", Statement.STATUS3, LocalDate.of(2022, 01, 10), 3),
new Person("per1", Statement.STATUS4, LocalDate.of(2022, 01, 10), 1), // 月份1的第二个per1事件
new Person("per1", Statement.STATUS1, LocalDate.of(2022, 02, 10), 1),
new Person("per2", Statement.STATUS1, LocalDate.of(2022, 03, 10), 1),
new Person("per3", Statement.STATUS2, LocalDate.of(2022, 03, 10), 2)
);
// 核心聚合逻辑
Map<Integer, PersonGroupMetric> res = src.stream()
.collect(groupingBy(
p -> p.eventDate().getMonthValue(), // 按月份分组
reducing(
PersonGroupMetric.EMPTY, // 初始值
PersonGroupMetric::new, // 映射器:将Person转换为PersonGroupMetric
PersonGroupMetric::add // 合并器:合并两个PersonGroupMetric
)
));
// 将聚合结果映射到最终的DTO
var fin = res.entrySet().stream()
.map(entry -> new DTO(
entry.getKey(), // 月份
entry.getValue().sum(), // 总和
entry.getValue().count() // 事件总数
))
.sorted((d1, d2) -> Integer.compare(d1.month(), d2.month())) // 按月份排序
.collect(toList());
// 打印结果
fin.forEach(System.out::println);
}
}代码解析:
src.stream(): 创建Person对象的Stream。
collect(groupingBy(...)): 这是主要的收集器,用于将Stream中的元素分组。
res.entrySet().stream().map(...): 聚合完成后,res是一个Map<Integer, PersonGroupMetric>。我们遍历其entrySet,将每个Map.Entry转换为最终的DTO对象。
sorted(...): 对结果按月份进行排序。
collect(toList()): 将最终的DTO对象收集到一个列表中。
运行上述代码,将得到如下输出:
DTO[month=1, totalSum=7, totalPersons=4] DTO[month=2, totalSum=1, totalPersons=1] DTO[month=3, totalSum=3, totalPersons=2]
这与我们期望的Total Sum和Person Count(事件数)完全一致。
事件计数 vs. 去重实体计数: 本教程中的PersonGroupMetric的count字段统计的是每个月发生的Person事件(即Person对象在列表中出现的次数)。如果您的需求是统计每个月去重后的Person ID数量(例如,per1在1月份出现了两次,但只算作1个不同的Person),则需要采用不同的聚合策略。例如,可以先按月份分组,然后对每个组内的Person ID进行Collectors.mapping和Collectors.toSet()操作,最后获取Set的大小。
// 示例:如果需要统计去重后的Person ID数量
Map<Integer, Long> distinctPersonCounts = src.stream()
.collect(groupingBy(
p -> p.eventDate().getMonthValue(),
mapping(Person::id, Collectors.collectingAndThen(Collectors.toSet(), Set::size).longValue()) // 错误的写法,需要Collectors.mapping
));
// 正确的去重Person ID计数
Map<Integer, Long> distinctPersonCountsCorrect = src.stream()
.collect(groupingBy(
p -> p.eventDate().getMonthValue(),
Collectors.mapping(Person::id, Collectors.collectingAndThen(Collectors.toSet(), Set::size).longValue()) // 错误的写法,需要Collectors.mapping
));
// 正确的去重Person ID计数
Map<Integer, Long> distinctPersonCountsFinal = src.stream()
.collect(groupingBy(
Person::getMonthValue, // 假设Person有一个getMonthValue方法
Collectors.mapping(Person::id, Collectors.collectingAndThen(Collectors.toSet(), Set::size))
));
// 实际实现
Map<Integer, Set<String>> distinctIdsByMonth = src.stream()
.collect(groupingBy(
p -> p.eventDate().getMonthValue(),
Collectors.mapping(Person::id, Collectors.toSet())
));
// 然后可以遍历 distinctIdsByMonth 来获取每个月的 distinct ID 数量
distinctIdsByMonth.forEach((month, ids) ->
System.out.println("Month " + month + ": Distinct Persons = " + ids.size())
);对于同时进行去重计数和求和,则需要一个更复杂的自定义Collector或多次Stream操作。
Collectors.reducing的强大之处:reducing收集器是处理复杂聚合任务的强大工具,它允许您定义一个初始值、一个将元素映射到累加器的方法,以及一个将两个累加器合并的方法。这使得它非常适合于需要同时计算多个指标的场景。
代码可读性与维护性: 通过将聚合逻辑封装在PersonGroupMetric这样的自定义度量类中,我们提高了代码的可读性和可维护性。聚合的细节被抽象出来,使得Stream管道本身更加清晰,专注于“如何分组”和“如何应用聚合”。
本教程展示了如何利用Java Stream API的groupingBy和reducing组合,优雅地解决多指标聚合问题。理解并熟练运用这些高级收集器,将极大地提升您处理数据集合的效率和代码质量。
以上就是Java Stream实现多指标聚合:按月汇总求和与计数的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号