0

0

Java Stream API:高效筛选列表中具有最新日期的唯一组合数据

DDD

DDD

发布时间:2025-09-12 09:51:22

|

192人浏览过

|

来源于php中文网

原创

Java Stream API:高效筛选列表中具有最新日期的唯一组合数据

本教程详细阐述了如何利用Java Stream API结合Collectors.toMap方法,从一个包含重复数据(基于特定字段组合)的列表中,高效筛选出每个唯一组合中具有最新日期(或其他条件)的记录。通过实例代码,演示了如何构建复合键、应用合并函数来解决复杂的数据去重与选择问题。

场景描述:处理复杂列表数据

在实际开发中,我们经常会遇到需要处理包含重复数据的列表。例如,一个员工列表中可能存在多条记录,它们拥有相同的姓氏和名字,但薪资或记录日期不同。我们的目标是,对于每一个唯一的姓氏和名字组合,只保留其中日期最新(或满足其他特定条件)的那条记录。

假设我们有一个Employee类定义如下:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDate;

@Data
@AllArgsConstructor
@NoArgsConstructor // 添加无参构造函数,方便Jackson等反序列化
public class Employee {
    private String firstName;
    private String lastName;
    private double salary;
    private LocalDate date; // 使用LocalDate表示日期

    // 方便测试的toString方法
    @Override
    public String toString() {
        return "Employee(" +
               "firstName='" + firstName + '\'' +
               ", lastName='" + lastName + '\'' +
               ", salary=" + salary +
               ", date=" + date +
               ')';
    }
}

现在,我们有一个Employee对象列表,其中包含一些具有相同firstName和lastName但date不同的记录:

List employees = new ArrayList<>();
employees.add(new Employee("John", "Smith", 10, LocalDate.of(2022, 9, 1)));
employees.add(new Employee("John", "Smith", 20, LocalDate.of(2022, 10, 1)));
employees.add(new Employee("John", "Smith", 5, LocalDate.of(2022, 11, 1)));
employees.add(new Employee("Kelly", "Jones", 12, LocalDate.of(2022, 3, 1)));
employees.add(new Employee("Sara", "Kim", 21, LocalDate.of(2022, 3, 1)));
employees.add(new Employee("Sara", "Kim", 7, LocalDate.of(2022, 7, 1)));

我们的目标是,对于"John Smith"、"Kelly Jones"和"Sara Kim"这三个唯一的姓名组合,分别找出日期最新的那条员工记录。预期输出应为:

  • "John", "Smith", 5, 2022-11-01
  • "Kelly", "Jones", 12, 2022-03-01
  • "Sara", "Kim", 7, 2022-07-01

核心工具:Java Stream API与Collectors.toMap()

Java 8引入的Stream API提供了一种声明式处理数据集合的强大方式。Collectors.toMap()是java.util.stream.Collectors类中一个非常实用的方法,它允许我们将流中的元素收集到一个Map中。toMap()有多个重载版本,其中最常用的是接受三个参数的版本:

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

  1. keyMapper (Function):用于从流元素中提取键的函数。
  2. valueMapper (Function):用于从流元素中提取值的函数。
  3. mergeFunction (BinaryOperator):一个合并函数,用于处理当两个或多个流元素映射到同一个键时如何解决冲突。

这个mergeFunction正是解决我们当前问题的关键。当多个Employee对象(例如,不同的"John Smith"记录)尝试映射到同一个键时,mergeFunction会介入,让我们决定保留哪一个。

解决方案详解与代码实现

我们将使用Collectors.toMap()来实现上述需求。

  1. 键的生成 (keyMapper) 为了确保每个唯一的姓氏和名字组合对应一个键,我们可以将firstName和lastName拼接成一个字符串作为键。例如,e -> e.getFirstName() + e.getLastName()。

  2. 值的映射 (valueMapper) 我们希望将整个Employee对象作为Map的值,因此可以使用Function.identity(),它会直接返回流中的当前元素。

  3. 合并函数 (mergeFunction) 这是最核心的部分。当两个Employee对象(e1和e2)映射到同一个键时,我们需要比较它们的date字段,并选择日期较新的那个。BinaryOperator会接收这两个冲突的Employee对象,并返回我们希望保留的那一个。表达式(e1, e2) -> e1.getDate().isAfter(e2.getDate()) ? e1 : e2正是实现了这一逻辑:如果e1的日期在e2之后,则保留e1,否则保留e2。

综合以上分析,完整的Stream操作代码如下:

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

// Employee 类定义如上所示 (需要Lombok的@Data, @AllArgsConstructor, @NoArgsConstructor)
// 为了代码完整性,这里再次包含Employee类定义
// @Data
// @AllArgsConstructor
// @NoArgsConstructor
// public class Employee {
//     private String firstName;
//     private String lastName;
//     private double salary;
//     private LocalDate date;
//
//     @Override
//     public String toString() {
//         return "Employee(" +
//                "firstName='" + firstName + '\'' +
//                ", lastName='" + lastName + '\'' +
//                ", salary=" + salary +
//                ", date=" + date +
//                ')';
//     }
// }

public class EmployeeFilterTutorial {

    public static void main(String[] args) {
        List employees = new ArrayList<>();
        employees.add(new Employee("John", "Smith", 10, LocalDate.of(2022, 9, 1)));
        employees.add(new Employee("John", "Smith", 20, LocalDate.of(2022, 10, 1)));
        employees.add(new Employee("John", "Smith", 5, LocalDate.of(2022, 11, 1)));
        employees.add(new Employee("Kelly", "Jones", 12, LocalDate.of(2022, 3, 1)));
        employees.add(new Employee("Sara", "Kim", 21, LocalDate.of(2022, 3, 1)));
        employees.add(new Employee("Sara", "Kim", 7, LocalDate.of(2022, 7, 1)));

        // 使用Stream API和Collectors.toMap()进行过滤
        Collection filteredEmployees = employees.stream()
                .collect(Collectors.toMap(
                        // keyMapper: 生成唯一键 (firstName + lastName)
                        e -> e.getFirstName() + e.getLastName(),
                        // valueMapper: 将Employee对象本身作为值
                        Function.identity(),
                        // mergeFunction: 处理键冲突,选择日期最新的Employee
                        (e1, e2) -> e1.getDate().isAfter(e2.getDate()) ? e1 : e2
                ))
                .values(); // 获取Map中的所有值,即我们筛选出的Employee列表

        // 打印结果
        filteredEmployees.forEach(System.out::println);
    }
}

输出结果:

PpcyAI
PpcyAI

泡泡次元AI-游戏美术AI创作平台,低门槛上手,高度可控,让你的创意秒速落地

下载
Employee(firstName='John', lastName='Smith', salary=5.0, date=2022-11-01)
Employee(firstName='Sara', lastName='Kim', salary=7.0, date=2022-07-01)
Employee(firstName='Kelly', lastName='Jones', salary=12.0, date=2022-03-01)

可以看到,输出结果与我们的预期完全一致,成功地为每个唯一的姓名组合筛选出了日期最新的员工记录。

注意事项与最佳实践

  1. 键的生成策略

    • 字符串拼接:如示例所示,e.getFirstName() + e.getLastName()简单直接,适用于字段数量不多且字段值不会包含分隔符导致歧义的情况。
    • 自定义复合键对象:对于更复杂的场景或需要更好的类型安全性,可以定义一个不可变的自定义类作为键,并正确实现equals()和hashCode()方法。例如:
      @Data
      @AllArgsConstructor
      @EqualsAndHashCode // Lombok自动生成equals和hashCode
      public class EmployeeKey {
          private String firstName;
          private String lastName;
      }
      // keyMapper: e -> new EmployeeKey(e.getFirstName(), e.getLastName())
    • 使用Map.entry()或AbstractMap.SimpleEntry:如果不想创建自定义类,也可以使用Map.entry(e.getFirstName(), e.getLastName())作为键,但需要注意其equals()和hashCode()的实现。
  2. 合并函数的灵活性mergeFunction不仅可以用于选择最新日期,还可以根据任何其他条件进行选择,例如:

    • 选择薪资最高的员工:(e1, e2) -> e1.getSalary() > e2.getSalary() ? e1 : e2
    • 选择日期最早的员工:(e1, e2) -> e1.getDate().isBefore(e2.getDate()) ? e1 : e2
    • 合并某些字段(例如,计算总薪资):(e1, e2) -> new Employee(e1.getFirstName(), e1.getLastName(), e1.getSalary() + e2.getSalary(), e1.getDate())(需要根据实际业务逻辑调整日期字段)。
  3. 性能考量Collectors.toMap()在内部会构建一个HashMap。对于非常大的数据集,这会占用额外的内存。然而,对于大多数常见场景,其性能是可接受的。如果内存是一个极其敏感的因素,可能需要考虑其他基于迭代的解决方案,但通常Stream API的简洁性和可读性带来的好处更大。

  4. 处理空值 在实际数据中,date字段可能为null。在合并函数中,如果直接调用e.getDate().isAfter(),可能会抛出NullPointerException。因此,在生产代码中,需要添加空值检查:

    (e1, e2) -> {
        if (e1.getDate() == null) return e2;
        if (e2.getDate() == null) return e1;
        return e1.getDate().isAfter(e2.getDate()) ? e1 : e2;
    }

    或者使用Comparator.nullsLast()等辅助方法。

  5. 替代方案:groupingBy结合reducing或maxBy 虽然toMap在此场景下非常简洁高效,但对于更复杂的聚合需求,Collectors.groupingBy()结合Collectors.reducing()或Collectors.maxBy()(配合Comparator)也是强大的选择。例如,使用groupingBy和maxBy:

    Collection filteredEmployeesAlternative = employees.stream()
        .collect(Collectors.groupingBy(
            e -> e.getFirstName() + e.getLastName(),
            Collectors.collectingAndThen(
                Collectors.maxBy(Comparator.comparing(Employee::getDate)),
                opt -> opt.orElse(null) // 处理Optional,如果分组为空则返回null
            )
        ))
        .values()
        .stream()
        .filter(java.util.Objects::nonNull) // 过滤掉可能存在的null值
        .collect(Collectors.toList());

    这种方式在语义上更明确地表达了“按组查找最大值”,但代码会稍微复杂一些。对于本教程中的“键冲突时选择一个”的场景,toMap的mergeFunction通常是更直接和简洁的选择。

总结

通过本教程,我们学习了如何利用Java Stream API的Collectors.toMap()方法,结合自定义的keyMapper和mergeFunction,高效地从列表中筛选出满足特定条件(如最新日期)的唯一记录。这种模式在处理数据去重、聚合和选择的复杂业务场景中非常有用,能够显著提升代码的简洁性和可读性。掌握Collectors.toMap()及其mergeFunction的用法,是深入理解和有效运用Java Stream API的关键一步。

热门AI工具

更多
DeepSeek
DeepSeek

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

豆包大模型
豆包大模型

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

通义千问
通义千问

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

腾讯元宝
腾讯元宝

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

文心一言
文心一言

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

讯飞写作
讯飞写作

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

即梦AI
即梦AI

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

ChatGPT
ChatGPT

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

相关专题

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

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

236

2023.09.22

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

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

458

2024.03.01

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

319

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

212

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1502

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

624

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

653

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

609

2024.04.29

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

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

8

2026.01.30

热门下载

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

精品课程

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

共23课时 | 3万人学习

C# 教程
C# 教程

共94课时 | 8万人学习

Java 教程
Java 教程

共578课时 | 53.4万人学习

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

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