0

0

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

聖光之護

聖光之護

发布时间:2025-08-05 14:10:01

|

598人浏览过

|

来源于php中文网

原创

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 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  Collector> toMap(
    Function keyMapper,
    Function valueMapper,
    BinaryOperator 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 最新的那个。

MuleRun
MuleRun

全球首个AI Agent交易平台

下载

我们可以使用 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 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 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。我们需要的是一个 List,因此我们通过 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
java

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

831

2023.06.15

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

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

737

2023.07.05

java自学难吗
java自学难吗

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

733

2023.07.31

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

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

397

2023.08.01

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

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

398

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有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

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

16925

2023.08.03

jQuery 正则表达式相关教程
jQuery 正则表达式相关教程

本专题整合了jQuery正则表达式相关教程大全,阅读专题下面的文章了解更多详细内容。

1

2026.01.13

热门下载

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

精品课程

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

共58课时 | 3.5万人学习

Pandas 教程
Pandas 教程

共15课时 | 0.9万人学习

ASP 教程
ASP 教程

共34课时 | 3.5万人学习

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

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