
业务场景概述
在实际开发中,我们经常会遇到需要处理包含重复数据的列表,但这些重复数据在某些关键字段上是唯一的。例如,一个员工列表可能包含同一个员工(通过姓氏和名字确定)在不同时间点的薪资记录。我们的目标是,对于每个唯一的员工组合(例如,相同的姓氏和名字),只保留其最新的一条记录。
假设我们有以下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;
}
} 我们的目标是得到以下结果:
立即学习“Java免费学习笔记(深入)”;
- "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的重载方法,该方法允许我们定义一个合并函数来处理键冲突。
核心思路
- 生成唯一的键: 对于每个Employee对象,我们需要一个唯一的键来表示一个特定的员工。在这个例子中,firstName和lastName的组合构成了这个键。
- 处理键冲突: 当两个或多个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);
}
} 代码解析
- employees.stream(): 创建一个Employee对象的流。
-
.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记录。
-
keyMapper (employee -> employee.getFirstName() + employee.getLastName()): 这个函数负责从每个Employee对象中提取出用于作为Map键的部分。在这里,我们将firstName和lastName字符串拼接起来,形成一个唯一的员工标识符。
-
.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,根据对象的多个字段进行分组,并从每个分组中筛选出符合特定条件的唯一记录。这种模式在数据处理和聚合中非常常见且实用。










