登录
首页 >  文章 >  java教程

Java多字段分组取最新记录技巧

时间:2025-09-13 13:36:45 300浏览 收藏

哈喽!大家好,很高兴又见面了,我是golang学习网的一名作者,今天由我给大家带来一篇《Java多字段分组筛选最新记录方法》,本文主要会讲到等等知识点,希望大家一起学习进步,也欢迎大家关注、点赞、收藏、转发! 下面就一起来看看吧!

使用Java Stream API按多字段分组并筛选最新记录

本教程将演示如何利用Java Stream API高效处理对象列表,根据对象的多个字段进行分组,并从每个分组中选取出满足特定条件的唯一记录。我们将以员工数据为例,展示如何通过Collectors.toMap结合自定义合并逻辑,实现按姓氏和名字组合筛选出最新薪资记录的需求。

业务场景概述

在实际开发中,我们经常会遇到需要处理包含重复数据的列表,但这些重复数据在某些关键字段上是唯一的。例如,一个员工列表可能包含同一个员工(通过姓氏和名字确定)在不同时间点的薪资记录。我们的目标是,对于每个唯一的员工组合(例如,相同的姓氏和名字),只保留其最新的一条记录。

假设我们有以下Employee类定义:

import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.LocalDate;

@Data
@AllArgsConstructor
public class Employee {
    private String firstName;
    private String lastName;
    private double salary;
    private LocalDate date; // 使用LocalDate表示日期
}

以及一个包含示例数据的List

import java.util.ArrayList;
import java.util.List;
import java.time.LocalDate;

public class EmployeeData {
    public static List getSampleEmployees() {
        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)));
        return employees;
    }
}

我们的目标是得到以下结果:

  • "John", "Smith", 5, 2022-11-01 (最新日期)
  • "Kelly", "Jones", 12, 2022-03-01 (唯一记录)
  • "Sara", "Kim", 7, 2022-07-01 (最新日期)

使用 Java Stream API 解决问题

Java 8 引入的 Stream API 为处理集合数据提供了强大而简洁的工具。对于上述需求,我们可以利用Collectors.toMap的重载方法,该方法允许我们定义一个合并函数来处理键冲突。

核心思路

  1. 生成唯一的键: 对于每个Employee对象,我们需要一个唯一的键来表示一个特定的员工。在这个例子中,firstName和lastName的组合构成了这个键。
  2. 处理键冲突: 当两个或多个Employee对象生成相同的键时(即它们是同一个员工),我们需要一个策略来决定保留哪一个。在本例中,我们希望保留日期最新的那条记录。

示例代码

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

public class EmployeeFilterTutorial {

    public static void main(String[] args) {
        List employees = EmployeeData.getSampleEmployees();

        Collection filteredEmployees = employees.stream()
                .collect(Collectors.toMap(
                        // keyMapper: 定义如何从Employee对象中提取键
                        // 这里将firstName和lastName拼接作为键
                        employee -> employee.getFirstName() + employee.getLastName(),
                        // valueMapper: 定义如何从Employee对象中提取值
                        // Function.identity() 表示Employee对象本身作为值
                        Function.identity(),
                        // mergeFunction: 定义当两个键相同时如何合并值
                        // 比较两个Employee对象的日期,保留日期最新的那个
                        (existingEmployee, newEmployee) -> existingEmployee.getDate().isAfter(newEmployee.getDate()) ? existingEmployee : newEmployee
                ))
                .values(); // 获取Map中所有的值,即我们筛选后的Employee对象

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

代码解析

  1. employees.stream(): 创建一个Employee对象的流。
  2. .collect(Collectors.toMap(...)): 这是核心操作。Collectors.toMap有多个重载方法,这里我们使用的是接受三个参数的版本:
    • keyMapper (employee -> employee.getFirstName() + employee.getLastName()): 这个函数负责从每个Employee对象中提取出用于作为Map键的部分。在这里,我们将firstName和lastName字符串拼接起来,形成一个唯一的员工标识符。
      • 注意事项: 字符串拼接作为键简单易行,但在某些情况下可能存在性能问题或潜在的哈希冲突风险(尽管对于人名组合通常足够)。更健壮的方法是定义一个包含firstName和lastName的自定义类作为键,并正确实现其equals()和hashCode()方法。
    • valueMapper (Function.identity()): 这个函数负责从Employee对象中提取出用于作为Map值的部分。Function.identity()是一个方便的静态方法,它表示将输入对象本身作为输出,即Employee对象本身将作为Map的值。
    • mergeFunction ((existingEmployee, newEmployee) -> existingEmployee.getDate().isAfter(newEmployee.getDate()) ? existingEmployee : newEmployee): 这是解决键冲突的关键。当keyMapper生成相同的键时,toMap会调用这个函数来决定保留哪个值。
      • existingEmployee:是Map中已经存在的值(即之前处理过的相同键的Employee对象)。
      • newEmployee:是当前正在处理的、与existingEmployee具有相同键的Employee对象。
      • 我们的逻辑是比较它们的date字段。如果existingEmployee的日期在newEmployee之后,则保留existingEmployee;否则,保留newEmployee。这确保了Map中最终存储的是具有最新日期的Employee记录。
  3. .values(): Collectors.toMap操作返回一个Map。我们只需要最终筛选出来的Employee对象,因此调用.values()方法获取Map中所有值的Collection。

运行结果

执行上述代码,将得到以下输出:

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)

这正是我们期望的结果,对于每个唯一的员工组合,都成功筛选出了日期最新的那条记录。

总结与注意事项

  • Collectors.toMap的强大之处: 通过提供一个自定义的合并函数,Collectors.toMap可以非常灵活地处理键冲突,这使其成为根据特定条件从分组数据中选择唯一记录的理想工具。
  • 灵活性: mergeFunction可以根据不同的业务需求进行调整。例如,如果你想保留薪资最高的记录,可以将比较逻辑改为existingEmployee.getSalary() > newEmployee.getSalary() ? existingEmployee : newEmployee。
  • 键的生成: 确保键的生成方式能够准确地识别你想要分组的唯一实体。对于多字段组合键,字符串拼接是一种简单的方法,但对于复杂对象或性能敏感的场景,自定义键对象并正确实现equals()和hashCode()会是更好的选择。
  • 替代方案: 另一种实现方式是先使用Collectors.groupingBy按多字段分组,得到Map>,然后再对每个List进行排序和选择。但对于本例这种直接选择一个“最佳”记录的场景,Collectors.toMap通常更简洁高效。

通过本教程,您应该已经掌握了如何使用Java Stream API结合Collectors.toMap,根据对象的多个字段进行分组,并从每个分组中筛选出符合特定条件的唯一记录。这种模式在数据处理和聚合中非常常见且实用。

文中关于的知识介绍,希望对你的学习有所帮助!若是受益匪浅,那就动动鼠标收藏这篇《Java多字段分组取最新记录技巧》文章吧,也可关注golang学习网公众号了解相关技术文章。

相关阅读
更多>
最新阅读
更多>
课程推荐
更多>