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 最新的那个。

AI Room Planner
AI Room Planner

AI 室内设计工具,免费为您的房间提供上百种设计方案

下载

我们可以使用 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 代码。

热门AI工具

更多
DeepSeek
DeepSeek

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

豆包大模型
豆包大模型

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

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

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

文心一言
文心一言

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

讯飞写作
讯飞写作

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

即梦AI
即梦AI

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

ChatGPT
ChatGPT

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

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

483

2023.08.02

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

237

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

458

2024.03.01

mysql标识符无效错误怎么解决
mysql标识符无效错误怎么解决

mysql标识符无效错误的解决办法:1、检查标识符是否被其他表或数据库使用;2、检查标识符是否包含特殊字符;3、使用引号包裹标识符;4、使用反引号包裹标识符;5、检查MySQL的配置文件等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

183

2023.12.04

Python标识符有哪些
Python标识符有哪些

Python标识符有变量标识符、函数标识符、类标识符、模块标识符、下划线开头的标识符、双下划线开头、双下划线结尾的标识符、整型标识符、浮点型标识符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

289

2024.02.23

java标识符合集
java标识符合集

本专题整合了java标识符相关内容,想了解更多详细内容,请阅读下面的文章。

259

2025.06.11

c++标识符介绍
c++标识符介绍

本专题整合了c++标识符相关内容,阅读专题下面的文章了解更多详细内容。

125

2025.08.07

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

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

75

2025.09.05

C++ 设计模式与软件架构
C++ 设计模式与软件架构

本专题深入讲解 C++ 中的常见设计模式与架构优化,包括单例模式、工厂模式、观察者模式、策略模式、命令模式等,结合实际案例展示如何在 C++ 项目中应用这些模式提升代码可维护性与扩展性。通过案例分析,帮助开发者掌握 如何运用设计模式构建高质量的软件架构,提升系统的灵活性与可扩展性。

14

2026.01.30

热门下载

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

精品课程

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

共58课时 | 4.3万人学习

Pandas 教程
Pandas 教程

共15课时 | 1.0万人学习

ASP 教程
ASP 教程

共34课时 | 4.2万人学习

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

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