0

0

使用Java 8 Stream API对自定义对象进行多属性分组与聚合操作指南

DDD

DDD

发布时间:2025-10-19 14:05:13

|

848人浏览过

|

来源于php中文网

原创

使用Java 8 Stream API对自定义对象进行多属性分组与聚合操作指南

本教程详细介绍了如何利用java 8 stream api对自定义对象进行多属性分组,并聚合特定字段的值。通过定义复合键对象和自定义累加器,结合`collectors.groupingby`和`collector.of`,可以高效地实现复杂的数据转换,将具有相同分组属性的对象合并为一个聚合对象,从而满足数据统计和处理的需求。

1. 引言:多属性分组与聚合的需求

在数据处理中,我们经常需要根据对象的多个属性进行分组,并对分组后的数据执行聚合操作(如求和、计数等)。例如,在一个学生列表中,我们可能需要根据学生的姓名、年龄和城市进行分组,然后计算每个分组中学生的总薪资和总奖金。Java 8引入的Stream API为这类操作提供了强大而灵活的工具

考虑以下Student类:

public class Student {
    private String name;
    private int age;
    private String city;
    private double salary;
    private double incentive;

    public Student(String name, int age, String city, double salary, double incentive) {
        this.name = name;
        this.age = age;
        this.city = city;
        this.salary = salary;
        this.incentive = incentive;
    }

    // Getters
    public String getName() { return name; }
    public int getAge() { return age; }
    public String getCity() { return city; }
    public double getSalary() { return salary; }
    public double getIncentive() { return incentive; }

    // Optional: toString for easy printing
    @Override
    public String toString() {
        return "Student{name='" + name + "', age=" + age + ", city='" + city + "', salary=" + salary + ", incentive=" + incentive + "}";
    }
}

我们的目标是将一个Student列表按name、age和city分组,并对salary和incentive进行求和。

2. 定义复合键对象

在Collectors.groupingBy操作中,我们需要一个能够唯一标识一组对象的键。当分组条件涉及多个属性时,简单地使用单个属性作为键是不足的。虽然可以使用List.of()(Java 9+)或Arrays.asList()(Java 8)将多个属性打包成一个列表作为键,但这通常会导致代码可读性差且不直观。

立即学习Java免费学习笔记(深入)”;

更清晰、更具维护性的方法是定义一个专门的类来表示这些复合键。对于Java 8环境,我们可以创建一个静态嵌套类NameAgeCity:

import java.util.Objects;

public static class NameAgeCity {
    private String name;
    private int age;
    private String city;

    public NameAgeCity(String name, int age, String city) {
        this.name = name;
        this.age = age;
        this.city = city;
    }

    // Getters
    public String getName() { return name; }
    public int getAge() { return age; }
    public String getCity() { return city; }

    // 静态工厂方法,方便从Student对象创建
    public static NameAgeCity from(Student s) {
        return new NameAgeCity(s.getName(), s.getAge(), s.getCity());
    }

    // 必须重写 equals 和 hashCode 方法,以确保分组的正确性
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        NameAgeCity that = (NameAgeCity) o;
        return age == that.age && Objects.equals(name, that.name) && Objects.equals(city, that.city);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, city);
    }

    @Override
    public String toString() {
        return "NameAgeCity{" + "name='" + name + '\'' + ", age=" + age + ", city='" + city + '\'' + '}';
    }
}

注意事项

  • equals()和hashCode()方法的正确实现对于Map键的正常工作至关重要。如果这两个方法没有正确重写,即使两个NameAgeCity实例包含相同的属性值,它们也可能被视为不同的键,导致分组失败。
  • from(Student s)静态工厂方法提供了一种方便的方式,将Student对象转换为我们的复合键对象。

3. 定义聚合值累加器

为了对salary和incentive进行求和,我们需要一个累加器来存储中间聚合结果。这个累加器将在Stream处理过程中,接收每个Student对象并更新其内部的聚合值。我们可以创建一个AggregatedValues类来实现这一功能:

import java.util.function.Consumer;

public static class AggregatedValues implements Consumer<Student> {
    private String name;
    private int age;
    private String city;
    private double salary;
    private double incentive;

    // 构造器,通常用于初始化
    public AggregatedValues() {
        this.salary = 0.0;
        this.incentive = 0.0;
    }

    // Getters
    public String getName() { return name; }
    public int getAge() { return age; }
    public String getCity() { return city; }
    public double getSalary() { return salary; }
    public double getIncentive() { return incentive; }

    // `accept` 方法用于累加单个 Student 对象的数据
    @Override
    public void accept(Student s) {
        // 首次遇到某个分组的Student时,初始化分组的标识信息
        if (name == null) this.name = s.getName();
        if (age == 0) this.age = s.getAge(); // 注意:如果age可能为0,需要更严谨的初始化逻辑
        if (city == null) this.city = s.getCity();
        this.salary += s.getSalary();
        this.incentive += s.getIncentive();
    }

    // `merge` 方法用于合并两个 AggregatedValues 实例(并行流场景)
    public AggregatedValues merge(AggregatedValues other) {
        this.salary += other.salary;
        this.incentive += other.incentive;
        return this;
    }

    // Optional: toString for easy printing
    @Override
    public String toString() {
        return "AggregatedValues{" + "name='" + name + '\'' + ", age=" + age + ", city='" + city + '\'' + ", salary=" + salary + ", incentive=" + incentive + '}';
    }
}

注意事项

  • AggregatedValues实现了Consumer<Student>接口,其accept方法定义了如何将一个Student对象的数据累加到当前实例中。
  • merge方法用于在并行流中合并不同线程计算出的部分结果。
  • name、age、city等字段在accept方法中进行条件初始化,确保它们只在第一次处理该分组的Student时被设置。对于age字段,如果原始数据中age可能为0,需要考虑更健壮的初始化逻辑,例如通过一个布尔标志位来判断是否已初始化。

4. 使用Collectors.groupingBy和Collector.of进行分组和聚合

现在,我们有了复合键NameAgeCity和累加器AggregatedValues,可以结合Java 8 Stream API的Collectors.groupingBy和Collector.of来实现最终的逻辑。

ModelGate
ModelGate

一站式AI模型管理与调用工具

下载

Collectors.groupingBy允许我们指定一个键映射函数和一个下游收集器(downstream collector)。在这里,键映射函数将Student对象转换为NameAgeCity,而下游收集器则负责将属于同一组的Student对象聚合成一个AggregatedValues实例。

Collector.of方法允许我们自定义一个收集器,它需要四个参数:

  1. supplier: 一个函数,用于创建新的结果容器(这里是AggregatedValues::new)。
  2. accumulator: 一个函数,用于将流中的元素累加到结果容器中(这里是AggregatedValues::accept)。
  3. combiner: 一个函数,用于合并两个结果容器(在并行流中特别有用,这里是AggregatedValues::merge)。
  4. finisher (可选): 一个函数,用于对最终结果容器进行转换(稍后介绍)。

完整的实现代码如下:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.function.Consumer;
import java.util.Objects; // For NameAgeCity

public class StudentAggregator {

    // Student 类定义 (同上)
    public static class Student {
        private String name;
        private int age;
        private String city;
        private double salary;
        private double incentive;

        public Student(String name, int age, String city, double salary, double incentive) {
            this.name = name;
            this.age = age;
            this.city = city;
            this.salary = salary;
            this.incentive = incentive;
        }

        public String getName() { return name; }
        public int getAge() { return age; }
        public String getCity() { return city; }
        public double getSalary() { return salary; }
        public double getIncentive() { return incentive; }

        @Override
        public String toString() {
            return "Student{name='" + name + "', age=" + age + ", city='" + city + "', salary=" + salary + ", incentive=" + incentive + "}";
        }
    }

    // NameAgeCity 类定义 (同上)
    public static class NameAgeCity {
        private String name;
        private int age;
        private String city;

        public NameAgeCity(String name, int age, String city) {
            this.name = name;
            this.age = age;
            this.city = city;
        }

        public String getName() { return name; }
        public int getAge() { return age; }
        public String getCity() { return city; }

        public static NameAgeCity from(Student s) {
            return new NameAgeCity(s.getName(), s.getAge(), s.getCity());
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            NameAgeCity that = (NameAgeCity) o;
            return age == that.age && Objects.equals(name, that.name) && Objects.equals(city, that.city);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name, age, city);
        }

        @Override
        public String toString() {
            return "NameAgeCity{" + "name='" + name + '\'' + ", age=" + age + ", city='" + city + '\'' + '}';
        }
    }

    // AggregatedValues 类定义 (同上)
    public static class AggregatedValues implements Consumer<Student> {
        private String name;
        private int age;
        private String city;
        private double salary;
        private double incentive;

        public AggregatedValues() {
            this.salary = 0.0;
            this.incentive = 0.0;
        }

        public String getName() { return name; }
        public int getAge() { return age; }
        public String getCity() { return city; }
        public double getSalary() { return salary; }
        public double getIncentive() { return incentive; }

        @Override
        public void accept(Student s) {
            if (name == null) this.name = s.getName();
            if (age == 0) this.age = s.getAge(); // 简化处理,实际可能需更严谨
            if (city == null) this.city = s.getCity();
            this.salary += s.getSalary();
            this.incentive += s.getIncentive();
        }

        public AggregatedValues merge(AggregatedValues other) {
            this.salary += other.salary;
            this.incentive += other.incentive;
            return this;
        }

        // 添加一个方法,将AggregatedValues转换回Student对象
        public Student toStudent() {
            return new Student(name, age, city, salary, incentive);
        }

        @Override
        public String toString() {
            return "AggregatedValues{" + "name='" + name + '\'' + ", age=" + age + ", city='" + city + '\'' + ", salary=" + salary + ", incentive=" + incentive + '}';
        }
    }

    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        Collections.addAll(students, // Java 8 兼容方式添加元素
            new Student("Raj", 10, "Pune", 10000, 100),
            new Student("Raj", 10, "Pune", 20000, 200),
            new Student("Raj", 20, "Pune", 10000, 100),
            new Student("Ram", 30, "Pune", 10000, 100),
            new Student("Ram", 30, "Pune", 30000, 300),
            new Student("Seema", 10, "Pune", 10000, 100)
        );

        // 使用 Collectors.groupingBy 和 Collector.of 进行分组和聚合
        List<Student> resultStudents = students.stream()
            .collect(Collectors.groupingBy(
                NameAgeCity::from,            // keyMapper: 将Student映射为复合键
                Collectors.of(                // downstream collector: 自定义聚合逻辑
                    AggregatedValues::new,    // supplier: 创建新的AggregatedValues实例
                    AggregatedValues::accept, // accumulator: 将Student数据累加到AggregatedValues
                    AggregatedValues::merge,  // combiner: 合并两个AggregatedValues实例
                    AggregatedValues::toStudent // finisher: 将最终的AggregatedValues转换回Student
                )
            ))
            .values()                         // 获取Map中的所有聚合值 (AggregatedValues实例)
            .stream()                         // 将Collection<Student>转换为Stream<Student>
            .collect(Collectors.toList());    // 收集为List<Student>

        resultStudents.forEach(System.out::println);
    }
}

输出结果:

Student{name='Raj', age=20, city='Pune', salary=10000.0, incentive=100.0}
Student{name='Raj', age=10, city='Pune', salary=30000.0, incentive=300.0}
Student{name='Ram', age=30, city='Pune', salary=40000.0, incentive=400.0}
Student{name='Seema', age=10, city='Pune', salary=10000.0, incentive=100.0}

5. 结果转换:使用finisher函数

在上述示例中,我们希望最终结果仍然是Student对象的列表,而不是AggregatedValues对象的列表。Collector.of的第四个参数finisher函数正为此目的而设计。它会在所有元素都被累加和合并之后,对最终的结果容器进行一次转换。

我们在AggregatedValues类中添加了一个toStudent()方法,用于将聚合后的数据封装回Student对象。

public static class AggregatedValues implements Consumer<Student> {
    // ... 其他代码 ...

    public Student toStudent() {
        return new Student(name, age, city, salary, incentive);
    }
}

然后,在Collector.of中,我们将AggregatedValues::toStudent作为finisher函数传入。这样,groupingBy操作的最终结果Map的value类型将直接是Student,省去了后续的map操作。

6. 总结与注意事项

通过本教程,我们学习了如何利用Java 8 Stream API对自定义对象进行多属性分组和聚合。关键点包括:

  • 自定义复合键对象:为多属性分组创建单独的键类(如NameAgeCity),并正确实现其equals()和hashCode()方法。
  • 自定义聚合累加器:创建一个累加器类(如AggregatedValues),用于在流处理过程中累加和合并数据。
  • Collectors.groupingBy与Collector.of:结合这两个强大的API,实现灵活的分组和自定义聚合逻辑。
  • finisher函数:利用Collector.of的finisher参数,在收集过程的最后一步将聚合结果转换为所需的最终对象类型。

这种方法不仅适用于数值求和,还可以扩展到其他聚合操作,如计算平均值、查找最大/最小值等,只需相应地调整AggregatedValues类中的accept和merge逻辑。它提供了一种结构清晰、易于维护且高效处理复杂数据转换的解决方案。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1954

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

658

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2401

2025.12.29

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

47

2026.01.19

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

765

2023.08.10

golang map内存释放
golang map内存释放

本专题整合了golang map内存相关教程,阅读专题下面的文章了解更多相关内容。

77

2025.09.05

golang map相关教程
golang map相关教程

本专题整合了golang map相关教程,阅读专题下面的文章了解更多详细内容。

40

2025.11.16

golang map原理
golang map原理

本专题整合了golang map相关内容,阅读专题下面的文章了解更多详细内容。

67

2025.11.17

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.4万人学习

C# 教程
C# 教程

共94课时 | 11.3万人学习

Java 教程
Java 教程

共578课时 | 81.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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