
在实际开发中,我们经常会遇到需要处理包含重复数据的列表。例如,一个员工列表中可能存在多条记录,它们拥有相同的姓氏和名字,但薪资或记录日期不同。我们的目标是,对于每一个唯一的姓氏和名字组合,只保留其中日期最新(或满足其他特定条件)的那条记录。
假设我们有一个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<Employee> 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"这三个唯一的姓名组合,分别找出日期最新的那条员工记录。预期输出应为:
Java 8引入的Stream API提供了一种声明式处理数据集合的强大方式。Collectors.toMap()是java.util.stream.Collectors类中一个非常实用的方法,它允许我们将流中的元素收集到一个Map中。toMap()有多个重载版本,其中最常用的是接受三个参数的版本:
立即学习“Java免费学习笔记(深入)”;
这个mergeFunction正是解决我们当前问题的关键。当多个Employee对象(例如,不同的"John Smith"记录)尝试映射到同一个键时,mergeFunction会介入,让我们决定保留哪一个。
我们将使用Collectors.toMap()来实现上述需求。
键的生成 (keyMapper) 为了确保每个唯一的姓氏和名字组合对应一个键,我们可以将firstName和lastName拼接成一个字符串作为键。例如,e -> e.getFirstName() + e.getLastName()。
值的映射 (valueMapper) 我们希望将整个Employee对象作为Map的值,因此可以使用Function.identity(),它会直接返回流中的当前元素。
合并函数 (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<Employee> 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<Employee> 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);
}
}输出结果:
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)
可以看到,输出结果与我们的预期完全一致,成功地为每个唯一的姓名组合筛选出了日期最新的员工记录。
键的生成策略
@Data
@AllArgsConstructor
@EqualsAndHashCode // Lombok自动生成equals和hashCode
public class EmployeeKey {
private String firstName;
private String lastName;
}
// keyMapper: e -> new EmployeeKey(e.getFirstName(), e.getLastName())合并函数的灵活性mergeFunction不仅可以用于选择最新日期,还可以根据任何其他条件进行选择,例如:
性能考量Collectors.toMap()在内部会构建一个HashMap。对于非常大的数据集,这会占用额外的内存。然而,对于大多数常见场景,其性能是可接受的。如果内存是一个极其敏感的因素,可能需要考虑其他基于迭代的解决方案,但通常Stream API的简洁性和可读性带来的好处更大。
处理空值 在实际数据中,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()等辅助方法。
替代方案:groupingBy结合reducing或maxBy 虽然toMap在此场景下非常简洁高效,但对于更复杂的聚合需求,Collectors.groupingBy()结合Collectors.reducing()或Collectors.maxBy()(配合Comparator)也是强大的选择。例如,使用groupingBy和maxBy:
Collection<Employee> 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的关键一步。
以上就是Java Stream API:高效筛选列表中具有最新日期的唯一组合数据的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号