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 {
    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接口,其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来实现最终的逻辑。

Supercreator
Supercreator

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 {
        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 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 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转换为Stream
            .collect(Collectors.toList());    // 收集为List

        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 {
    // ... 其他代码 ...

    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逻辑。它提供了一种结构清晰、易于维护且高效处理复杂数据转换的解决方案。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

844

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

742

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

740

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

400

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

431

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

php远程文件教程合集
php远程文件教程合集

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

21

2026.01.22

热门下载

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

精品课程

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

共23课时 | 2.8万人学习

C# 教程
C# 教程

共94课时 | 7.3万人学习

Java 教程
Java 教程

共578课时 | 49.7万人学习

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

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