首页 > Java > java教程 > 正文

Java Stream 进阶:优雅地移除重复对象并保留最新记录

聖光之護
发布: 2025-08-05 14:10:01
原创
590人浏览过

Java Stream 进阶:优雅地移除重复对象并保留最新记录

本教程详细阐述如何利用 Java Stream API 高效处理列表中具有重复ID的对象,并仅保留每个ID对应的最新记录。我们将重点介绍 Collectors.toMap 的三参数版本,结合 BinaryOperator.maxBy 和 Comparator.comparing,以声明式方式实现复杂的去重逻辑,确保数据完整性和代码简洁性。

引言:处理列表对象去重的挑战

在数据处理中,我们经常遇到需要从列表中移除重复项的场景。然而,简单的去重往往不能满足所有需求。例如,当列表中存在多个具有相同标识符(id)的对象时,我们可能需要根据某个特定属性(如时间戳)来决定保留哪一个。本教程将聚焦于一个典型场景:给定一个包含 student 对象的列表,每个 student 对象都有一个 id 和一个 startdatetime。如果存在多个 student 对象具有相同的 id,我们希望只保留其中 startdatetime 最新的那个。

考虑以下 Student 类定义及其示例数据:

package org.example;

import java.time.LocalDateTime;
import java.util.Objects; // 导入 Objects 类

public class Student {
    private String id;
    private LocalDateTime startDatetime;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public LocalDateTime getStartDatetime() {
        return startDatetime;
    }

    public void setStartDatetime(LocalDateTime startDatetime) {
        this.startDatetime = startDatetime;
    }

    public Student(String id, LocalDateTime startDatetime) {
        this.id = id;
        this.startDatetime = startDatetime;
    }

    @Override
    public String toString() {
        return "Student{id='" + id + "', startDatetime=" + startDatetime + '}';
    }

    // 建议重写 equals 和 hashCode,尽管本例中不是必需的,但对于集合操作是良好实践
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(id, student.id) && Objects.equals(startDatetime, student.startDatetime);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, startDatetime);
    }
}
登录后复制

初始数据示例如下:

List<Student> students = List.of(
    new Student("1", LocalDateTime.now()),
    new Student("1", LocalDateTime.of(2000, 2, 1, 1, 1)),
    new Student("1", LocalDateTime.of(1990, 2, 1, 1, 1)),
    new Student("2", LocalDateTime.of(1990, 2, 1, 1, 1))
);
登录后复制

我们期望的结果是:对于ID为"1"的学生,保留 LocalDateTime.now() 对应的记录;对于ID为"2"的学生,保留其唯一的记录。最终列表应只包含两条记录。

核心解决方案:Collectors.toMap 的三参数用法

Java Stream API 提供了强大而灵活的 Collectors 工具类,其中 Collectors.toMap 的三参数版本是解决此类问题的关键。其方法签名通常为:

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

public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
    Function<? super T, ? extends K> keyMapper,
    Function<? super T, ? extends U> valueMapper,
    BinaryOperator<U> mergeFunction
)
登录后复制

这个方法接受三个参数:

  1. keyMapper:一个函数,用于从流中的每个元素(T 类型)提取出作为 Map 键(K 类型)的值。
  2. valueMapper:一个函数,用于从流中的每个元素(T 类型)提取出作为 Map 值(U 类型)的值。
  3. mergeFunction:一个二元操作符,用于处理当两个或多个流元素映射到相同的键时如何合并它们的值。这是解决我们去重逻辑的关键所在。

1. keyMapper:提取 ID 作为键

对于我们的 Student 对象,我们希望根据 id 进行去重。因此,keyMapper 应该是一个从 Student 对象中获取其 id 的函数引用:Student::getId。

2. valueMapper:保留原始对象作为值

我们希望在去重后保留完整的 Student 对象,而不是其某个属性。因此,valueMapper 应该简单地返回原始 Student 对象本身。这可以通过 Function.identity() 实现。

3. mergeFunction:解决冲突并保留最新记录

这是最核心的部分。当 Collectors.toMap 遇到具有相同键(id)的多个 Student 对象时,mergeFunction 会被调用来决定保留哪一个。我们的目标是保留 startDatetime 最新的那个。

Hour One
Hour One

AI文字到视频生成

Hour One 37
查看详情 Hour One

我们可以使用 BinaryOperator.maxBy 结合 Comparator.comparing 来实现这一逻辑:

  • Comparator.comparing(Student::getStartDatetime):这会创建一个 Comparator,用于比较两个 Student 对象的 startDatetime 属性。
  • BinaryOperator.maxBy(Comparator.comparing(Student::getStartDatetime)):BinaryOperator.maxBy 接受一个 Comparator,并返回一个 BinaryOperator,该操作符会在两个输入值中选择由 Comparator 定义的“最大”值。在这里,“最大”意味着 startDatetime 最新的 Student 对象。

因此,mergeFunction 的完整表达式为:BinaryOperator.maxBy(Comparator.comparing(Student::getStartDatetime))。

实战代码示例

将上述概念整合到完整的 Java 代码中:

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Main {

    // Student 类定义(与上面保持一致)
    public static class Student {
        private String id;
        private LocalDateTime startDatetime;

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public LocalDateTime getStartDatetime() {
            return startDatetime;
        }

        public void setStartDatetime(LocalDateTime startDatetime) {
            this.startDatetime = startDatetime;
        }

        public Student(String id, LocalDateTime startDatetime) {
            this.id = id;
            this.startDatetime = startDatetime;
        }

        @Override
        public String toString() {
            return "Student{id='" + id + "', startDatetime=" + startDatetime + '}';
        }
    }

    public static void main(String[] args) {
        // 原始学生列表
        List<Student> students = new ArrayList<>() {{
            add(new Student("1", LocalDateTime.now())); // 最新的ID为1的记录
            add(new Student("1", LocalDateTime.of(2000, 2, 1, 1, 1)));
            add(new Student("1", LocalDateTime.of(1990, 2, 1, 1, 1)));
            add(new Student("2", LocalDateTime.of(1990, 2, 1, 1, 1)));
            add(new Student("3", LocalDateTime.of(2020, 1, 1, 0, 0))); // 新增一个不重复的记录
            add(new Student("3", LocalDateTime.of(2019, 1, 1, 0, 0))); // 较旧的ID为3的记录
        }};

        System.out.println("原始学生列表:");
        students.forEach(System.out::println);
        System.out.println("--------------------");

        // 使用 Stream API 去重并保留最新记录
        List<Student> uniqueStudents = students.stream()
            .collect(Collectors.toMap(
                Student::getId,                                         // keyMapper: 以ID作为键
                Function.identity(),                                    // valueMapper: 保留原始Student对象作为值
                BinaryOperator.maxBy(Comparator.comparing(Student::getStartDatetime)) // mergeFunction: 冲突时保留startDatetime最新的
            ))
            .values()                                                   // 获取Map中所有的值(去重后的Student对象)
            .stream()                                                   // 将值集合转换为新的Stream
            .sorted(Comparator.comparing(Student::getStartDatetime))    // 可选:根据startDatetime排序结果
            .toList(); // Java 16+ 新特性,等价于 .collect(Collectors.toList())

        System.out.println("去重并保留最新记录后的学生列表:");
        uniqueStudents.forEach(System.out::println);
    }
}
登录后复制

运行结果示例(LocalDateTime.now() 会根据运行时间变化):

原始学生列表:
Student{id='1', startDatetime=2023-10-27T10:30:45.123456}
Student{id='1', startDatetime=2000-02-01T01:01}
Student{id='1', startDatetime=1990-02-01T01:01}
Student{id='2', startDatetime=1990-02-01T01:01}
Student{id='3', startDatetime=2020-01-01T00:00}
Student{id='3', startDatetime=2019-01-01T00:00}
--------------------
去重并保留最新记录后的学生列表:
Student{id='2', startDatetime=1990-02-01T01:01}
Student{id='1', startDatetime=2023-10-27T10:30:45.123456} // 此处的日期时间会是运行时的当前时间
Student{id='3', startDatetime=2020-01-01T00:00}
登录后复制

可以看到,ID为"1"和"3"的重复记录已被成功去重,并保留了 startDatetime 最新的那一条。最终列表也根据 startDatetime 进行了排序。

结果转换与后续处理

在上述代码中,collect(Collectors.toMap(...)) 的结果是一个 Map<String, Student>。我们需要的是一个 List<Student>,因此我们通过 Map.values() 获取到 Map 中所有去重后的 Student 对象集合,然后将其转换为一个新的 Stream,并最终收集为 List。

  • .values():返回 Map 中所有值的 Collection 视图。
  • .stream():将这个 Collection 转换为一个新的 Stream。
  • .sorted(Comparator.comparing(Student::getStartDatetime)):这是一个可选步骤,用于对最终结果列表按照 startDatetime 进行升序排序。如果不需要特定顺序,可以省略此步骤。
  • .toList():Java 16 引入的便捷方法,用于将 Stream 收集为不可变的 List。对于早期 Java 版本,可以使用 collect(Collectors.toList())。

注意事项与最佳实践

  1. 性能考量:Collectors.toMap 内部会构建一个 HashMap 来存储中间结果。对于非常大的数据集,这会产生一定的内存开销。然而,对于大多数常见场景,这种方式的性能表现是可接受的,并且其代码的简洁性优势显著。
  2. 可读性与维护性:使用 Stream API 结合 Collectors.toMap 能够以声明式的方式表达复杂的业务逻辑,使得代码意图清晰,易于理解和维护,避免了传统循环中常见的嵌套条件判断。
  3. 空值处理:如果 keyMapper 或 valueMapper 可能返回 null,或者 Comparator 在比较时遇到 null,可能会抛出 NullPointerException。在实际应用中,需要根据具体业务需求进行 null 值检查或处理。例如,如果 startDatetime 可能为 null,可以使用 Comparator.nullsFirst() 或 Comparator.nullsLast()。
  4. Java 版本兼容性:.toList() 方法是 Java 16 及更高版本引入的。如果您的项目使用较早的 Java 版本(如 Java 8 或 11),请使用 collect(Collectors.toList())。
  5. 通用性:这种模式不仅适用于根据日期去重,还可以根据任何可比较的属性(如版本号、优先级等)去重,只需调整 Comparator 的逻辑即可。例如,如果需要保留“最小”值,可以使用 BinaryOperator.minBy。

总结

通过本教程,我们深入探讨了如何利用 Java Stream API 中的 Collectors.toMap 的三参数版本,结合 Function.identity() 和 BinaryOperator.maxBy(Comparator.comparing(...)),优雅且高效地解决列表中对象去重并保留最新记录的问题。这种声明式编程风格不仅提升了代码的简洁性和可读性,也充分展现了 Java Stream 在处理复杂集合操作时的强大能力。掌握这一模式,将有助于您在日常开发中编写出更加健壮和现代的 Java 代码。

以上就是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号