首页 > Java > java教程 > 正文

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

DDD
发布: 2025-10-19 14:05:13
原创
819人浏览过

使用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来实现最终的逻辑。

标书对比王
标书对比王

标书对比王是一款标书查重工具,支持多份投标文件两两相互比对,重复内容高亮标记,可快速定位重复内容原文所在位置,并可导出比对报告。

标书对比王 58
查看详情 标书对比王

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

以上就是使用Java 8 Stream API对自定义对象进行多属性分组与聚合操作指南的详细内容,更多请关注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号