首页 > Java > java教程 > 正文

Java Stream实现多指标聚合:按月汇总求和与计数

聖光之護
发布: 2025-10-17 10:04:19
原创
509人浏览过

Java Stream实现多指标聚合:按月汇总求和与计数

本文详细介绍了如何利用java stream api,特别是collectors.groupingby与collectors.reducing组合,高效地对数据进行多条件聚合。通过构建自定义度量类,我们能够同时实现按月份对特定数值进行求和,并统计对应事件的数量。教程涵盖了数据模型定义、聚合逻辑实现以及最终结果的映射转换,并强调了在计数场景中事件计数与去重实体计数的区别

在现代Java应用中,对集合数据进行高效的统计、分组和聚合是常见的需求。Java Stream API提供了强大而灵活的工具来处理这类任务。本教程将深入探讨如何使用Collectors.groupingBy结合Collectors.reducing来实现复杂的数据聚合,例如按月份分组,并同时计算某个值的总和以及组内元素的数量。

1. 场景与需求分析

假设我们有一个Person对象的列表,每个Person对象包含ID、事件类型、事件日期和关联值。我们的目标是:

  1. 按事件发生的月份进行分组。
  2. 统计每个月所有Person对象的value总和。
  3. 统计每个月发生的Person事件总数。

原始数据示例如下:

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数量。

2. 数据模型定义

首先,我们需要定义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在此表示事件计数
登录后复制

3. 构建自定义聚合度量类 PersonGroupMetric

为了在一次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的设计是关键:

  • EMPTY常量提供了reducing操作的起始点。
  • 构造函数PersonGroupMetric(Person p)将每个Person对象转换为一个包含其自身计数(1)和值(p.value())的度量实例。
  • add方法定义了如何将两个PersonGroupMetric实例合并,即简单地将它们的count和sum相加。

4. 使用 Stream API 进行聚合

有了数据模型和度量类,我们现在可以使用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);
    }
}
登录后复制

代码解析:

腾讯智影-AI数字人
腾讯智影-AI数字人

基于AI数字人能力,实现7*24小时AI数字人直播带货,低成本实现直播业务快速增增,全天智能在线直播

腾讯智影-AI数字人 73
查看详情 腾讯智影-AI数字人
  1. src.stream(): 创建Person对象的Stream。

  2. collect(groupingBy(...)): 这是主要的收集器,用于将Stream中的元素分组。

    • p -> p.eventDate().getMonthValue(): 这是分组键的函数,它提取Person对象的月份作为分组依据。
    • reducing(...): 这是groupingBy的下游收集器,负责在每个组内执行聚合。
      • PersonGroupMetric.EMPTY: reducing操作的初始累加器值。
      • PersonGroupMetric::new: 映射函数,将Stream中的每个Person对象转换(映射)为一个PersonGroupMetric实例。
      • PersonGroupMetric::add: 合并函数,用于将两个PersonGroupMetric实例合并成一个。这正是count和sum进行累加的地方。
  3. res.entrySet().stream().map(...): 聚合完成后,res是一个Map<Integer, PersonGroupMetric>。我们遍历其entrySet,将每个Map.Entry转换为最终的DTO对象。

  4. sorted(...): 对结果按月份进行排序。

  5. 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(事件数)完全一致。

5. 注意事项与总结

  • 事件计数 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中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号