
本教程详细介绍了如何在java中高效地从一个对象列表中移除那些其特定键值在另一个对象列表中不存在的项。文章涵盖了Java 8及更高版本中利用stream api的现代化解决方案,以及java 8之前版本通过传统循环和迭代器实现的方法,并对不同方法的性能和适用场景进行了深入分析,旨在帮助开发者选择最优化策略。
1. 定义数据模型
首先,我们定义两个用于示例的java类,它们分别代表不同类型的数据实体。
public class RetailerexcelConversionDto { private String retailerCode; // 零售商编码 private Integer isActive; // 是否活跃 // 构造函数、Getter和Setter public RetailerExcelConversionDto(String retailerCode, Integer isActive) { this.retailerCode = retailerCode; this.isActive = isActive; } public String getRetailerCode() { return retailerCode; } public void setRetailerCode(String retailerCode) { this.retailerCode = retailerCode; } public Integer getIsActive() { return isActive; } public void setIsActive(Integer isActive) { this.isActive = isActive; } } public class RetailerDto { private String code; // 零售商编码 private Integer age; // 年龄 private String name; // 名称 // 构造函数、Getter和Setter public RetailerDto(String code, Integer age, String name) { this.code = code; this.age = age; this.name = name; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
假设我们有以下两个列表实例:
List<RetailerExcelConversionDto> retailerConversionDtoList = new ArrayList<>(); // 填充数据... retailerConversionDtoList.add(new RetailerExcelConversionDto("R001", 1)); retailerConversionDtoList.add(new RetailerExcelConversionDto("R002", 1)); retailerConversionDtoList.add(new RetailerExcelConversionDto("R003", 0));  List<RetailerDto> retailerDtoList = new ArrayList<>(); // 填充数据... retailerDtoList.add(new RetailerDto("R001", 30, "Retailer A")); retailerDtoList.add(new RetailerDto("R004", 25, "Retailer D"));
2. 核心问题阐述
我们的目标是从 retailerConversionDtoList 中移除所有 retailerCode 不存在于 retailerDtoList 中任何 code 的项。简而言之,我们希望保留 retailerConversionDtoList 中与 retailerDtoList 存在关联(通过编码匹配)的项。
3. 解决方案一:使用 Java 8 stream API (推荐)
在 Java 8 及更高版本中,Stream API 提供了一种简洁且高效的方式来处理集合数据。这种方法利用 Set 的快速查找特性,优化了移除操作。
立即学习“Java免费学习笔记(深入)”;
实现步骤:
- 从 retailerDtoList 中提取所有 code 属性,并将它们收集到一个 Set<String> 中。使用 Set 的目的是为了实现 O(1) 的平均查找时间复杂度,而非 List 的 O(N),这在数据量较大时能显著提升性能。
 - 使用 Stream API 过滤 retailerConversionDtoList。对于 retailerConversionDtoList 中的每个元素,检查其 retailerCode 是否存在于第一步创建的 Set 中。
 - 将过滤后的元素收集到一个新的 List 中。
 
示例代码:
import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.ArrayList; import java.util.HashSet; // 导入 HashSet 以便构建示例数据  // 假设 RetailerExcelConversionDto 和 RetailerDto 类已定义  public class ListRemovalWithStream {      public static void main(String[] args) {         // 示例数据         List<RetailerExcelConversionDto> retailerConversionDtoList = new ArrayList<>();         retailerConversionDtoList.add(new RetailerExcelConversionDto("R001", 1));         retailerConversionDtoList.add(new RetailerExcelConversionDto("R002", 1));         retailerConversionDtoList.add(new RetailerExcelConversionDto("R003", 0));         System.out.println("原始 retailerConversionDtoList: " + retailerConversionDtoList.stream().map(RetailerExcelConversionDto::getRetailerCode).collect(Collectors.toList()));          List<RetailerDto> retailerDtoList = new ArrayList<>();         retailerDtoList.add(new RetailerDto("R001", 30, "Retailer A"));         retailerDtoList.add(new RetailerDto("R004", 25, "Retailer D"));         System.out.println("参照 retailerDtoList codes: " + retailerDtoList.stream().map(RetailerDto::getCode).collect(Collectors.toList()));          // 步骤 1: 提取所有存在的 retailer codes 到一个 Set 中         Set<String> retailerCodes = retailerDtoList.stream()                                                     .map(RetailerDto::getCode)                                                     .collect(Collectors.toSet());          // 步骤 2 & 3: 过滤 retailerConversionDtoList         retailerConversionDtoList = retailerConversionDtoList.stream()                                                             .Filter(t -> retailerCodes.contains(t.getRetailerCode()))                                                             .collect(Collectors.toList());          System.out.println("过滤后的 retailerConversionDtoList: " + retailerConversionDtoList.stream().map(RetailerExcelConversionDto::getRetailerCode).collect(Collectors.toList()));         // 预期输出: [R001]     } }
优点:
- 代码简洁: Stream API 提供了声明式编程风格,使代码更易读、更简洁。
 - 效率高: 利用 Set 进行查找,时间复杂度接近 O(N + M),其中 N 和 M 分别是两个列表的大小。
 - 函数式编程: 更好地支持函数式编程范式,易于并行化(尽管在此场景下可能不需要)。
 
4. 解决方案二:Java 8 之前的传统方法
对于不支持 Java 8 Stream API 的旧版本 Java 环境,我们可以通过传统的循环和迭代器来实现相同的功能。
4.1 方法一:构建新列表
这种方法与 Stream API 的逻辑类似,都是先构建一个用于查找的 Set,然后遍历原列表,将符合条件的元素添加到一个新列表中。
实现步骤:
- 创建一个 HashSet<String> 用于存储 retailerDtoList 中所有 code。
 - 遍历 retailerDtoList,将每个 RetailerDto 对象的 code 添加到 HashSet 中。
 - 创建一个新的 List<RetailerExcelConversionDto> 来存储过滤后的结果。
 - 遍历 retailerConversionDtoList。对于每个元素,检查其 retailerCode 是否存在于 HashSet 中。如果存在,则将其添加到新列表中。
 
示例代码:
import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set;  public class ListRemovalTraditionalNewList {      public static void main(String[] args) {         // 示例数据 (同上)         List<RetailerExcelConversionDto> retailerConversionDtoList = new ArrayList<>();         retailerConversionDtoList.add(new RetailerExcelConversionDto("R001", 1));         retailerConversionDtoList.add(new RetailerExcelConversionDto("R002", 1));         retailerConversionDtoList.add(new RetailerExcelConversionDto("R003", 0));         System.out.println("原始 retailerConversionDtoList: " + retailerConversionDtoList.stream().map(RetailerExcelConversionDto::getRetailerCode).collect(Collectors.toList()));          List<RetailerDto> retailerDtoList = new ArrayList<>();         retailerDtoList.add(new RetailerDto("R001", 30, "Retailer A"));         retailerDtoList.add(new RetailerDto("R004", 25, "Retailer D"));         System.out.println("参照 retailerDtoList codes: " + retailerDtoList.stream().map(RetailerDto::getCode).collect(Collectors.toList()));          // 步骤 1 & 2: 提取所有存在的 retailer codes 到一个 Set 中         Set<String> retailerCodes = new HashSet<>();         for (RetailerDto retailer : retailerDtoList) {             retailerCodes.add(retailer.getCode());         }          // 步骤 3 & 4: 遍历并构建新列表         List<RetailerExcelConversionDto> newRetailerConversionDtoList = new ArrayList<>();         for (RetailerExcelConversionDto item : retailerConversionDtoList) {             if (retailerCodes.contains(item.getRetailerCode())) {                 newRetailerConversionDtoList.add(item);             }         }         retailerConversionDtoList = newRetailerConversionDtoList; // 更新引用到新列表          System.out.println("过滤后的 retailerConversionDtoList: " + retailerConversionDtoList.stream().map(RetailerExcelConversionDto::getRetailerCode).collect(Collectors.toList()));         // 预期输出: [R001]     } }
优点:
- 兼容性好: 适用于所有 Java 版本。
 - 逻辑清晰: 循环和条件判断直观易懂。
 
缺点:
- 内存开销: 需要创建一个新的列表,如果原始列表很大,可能会有额外的内存开销。
 - 代码冗长: 相较于 Stream API,代码量更多。
 
4.2 方法二:使用迭代器原地移除
如果需要直接修改原始列表而不是创建新列表,可以使用迭代器(Iterator)的 remove() 方法。注意:在循环遍历集合时,直接使用增强for循环(for-each)或普通 for 循环通过索引移除元素会导致 ConcurrentModificationException 或跳过元素。 迭代器的 remove() 方法是唯一安全的在遍历时修改集合的方式。
实现步骤:
- 创建一个 HashSet<String> 用于存储 retailerDtoList 中所有 code。
 - 获取 retailerConversionDtoList 的迭代器。
 - 使用 while (iterator.hasNext()) 循环遍历列表。
 - 在循环中,获取当前元素,并检查其 retailerCode 是否存在于 HashSet 中。
 - 如果 retailerCode 不存在于 HashSet 中,则使用 iterator.remove() 方法将当前元素从列表中移除。
 
示例代码:
import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set;  public class ListRemovalTraditionalIterator {      public static void main(String[] args) {         // 示例数据 (同上)         List<RetailerExcelConversionDto> retailerConversionDtoList = new ArrayList<>();         retailerConversionDtoList.add(new RetailerExcelConversionDto("R001", 1));         retailerConversionDtoList.add(new RetailerExcelConversionDto("R002", 1));         retailerConversionDtoList.add(new RetailerExcelConversionDto("R003", 0));         System.out.println("原始 retailerConversionDtoList: " + retailerConversionDtoList.stream().map(RetailerExcelConversionDto::getRetailerCode).collect(Collectors.toList()));          List<RetailerDto> retailerDtoList = new ArrayList<>();         retailerDtoList.add(new RetailerDto("R001", 30, "Retailer A"));         retailerDtoList.add(new RetailerDto("R004", 25, "Retailer D"));         System.out.println("参照 retailerDtoList codes: " + retailerDtoList.stream().map(RetailerDto::getCode).collect(Collectors.toList()));          // 步骤 1: 提取所有存在的 retailer codes 到一个 Set 中         Set<String> retailerCodes = new HashSet<>();         for (RetailerDto retailer : retailerDtoList) {             retailerCodes.add(retailer.getCode());         }          // 步骤 2-5: 使用迭代器原地移除         for (Iterator<RetailerExcelConversionDto> it = retailerConversionDtoList.iterator(); it.hasNext(); ) {             RetailerExcelConversionDto next = it.next();             if (!retailerCodes.contains(next.getRetailerCode())) {                 it.remove(); // 如果不存在,则移除             }         }          System.out.println("过滤后的 retailerConversionDtoList: " + retailerConversionDtoList.stream().map(RetailerExcelConversionDto::getRetailerCode).collect(Collectors.toList()));         // 预期输出: [R001]     } }
优点:
- 节省内存: 直接修改原始列表,无需创建新列表。
 - 兼容性好: 适用于所有 Java 版本。
 
缺点:
- 代码相对复杂: 迭代器操作不如 Stream API 直观。
 - 易错性: 如果不正确使用迭代器(例如,在增强for循环中尝试修改),很容易导致运行时错误。
 
5. 性能考量与最佳实践
- 使用 Set 进行查找: 无论采用哪种方法,将参照列表的键值收集到 HashSet 中都是至关重要的优化步骤。HashSet 提供了平均 O(1) 的查找时间复杂度,而 ArrayList 或 LinkedList 的 contains() 方法是 O(N),这将极大地影响整体性能,尤其是在列表数据量较大时。
 - Java 8+ 优先选择 Stream API: Stream API 结合 filter 和 collect 提供了最简洁、可读性高且性能优越的解决方案。它避免了手动管理迭代器,并且在内部实现上通常经过优化。
 -  旧版本 Java 的选择:
- 如果内存不是瓶颈,并且希望代码更直观,可以选择“构建新列表”的方法。
 - 如果对内存使用有严格要求,且愿意处理迭代器逻辑,可以选择“使用迭代器原地移除”的方法。务必正确使用 Iterator.remove()。
 
 
6. 总结
本文详细介绍了在 Java 中根据关联键从一个对象列表中移除项的多种方法。对于 Java 8 及更高版本,推荐使用 Stream API 结合 Set 进行高效过滤,它提供了最佳的简洁性和性能平衡。对于旧版本 Java,通过将参照键存储在 HashSet 中,然后选择构建新列表或使用迭代器原地移除,也能实现相同的目标。理解不同方法的优缺点和适用场景,有助于开发者在实际项目中做出明智的选择,编写出更健壮、高效的代码。


