retainall不是查交集的正解,它是原地修改左操作数、只保留与右操作数共有元素的破坏性操作,不返回新集合,且存在不可变列表抛异常、性能差、依赖equals/hashcode等风险。

retainAll 方法到底是不是“查交集”的正解
不是,retainAll 是“原地修改左操作数,只保留它和右操作数共有的元素”,本质是破坏性操作,不是纯函数式交集计算。它改的是调用方本身,不是返回新集合。
常见错误现象:listA.retainAll(listB) 后 listA 变了,但你原本想留着 listA 做别的事——这时候已经晚了。
- 如果
listA是Arrays.asList()返回的不可变列表,调用retainAll会直接抛UnsupportedOperationException - 如果
listA是LinkedList,retainAll时间复杂度是 O(n×m),比用HashSet预处理慢得多 - 它依赖
equals()和hashCode(),自定义对象没重写这两个方法时,结果永远为空
想安全查交集,应该先转成 Set 再操作
交集的核心是“存在性判断”,HashSet 的 contains() 是 O(1),比遍历 List 快一个数量级。Java 8+ 推荐用 Stream + Collectors.toSet() 链式构造。
使用场景:需要保留原始 List 不变、交集结果要可重复使用、或后续还要做并集/差集等其他集合运算。
立即学习“Java免费学习笔记(深入)”;
- 先确保两个列表非 null,否则
stream()会 NPE;空列表可直接返回空ArrayList - 小数据量(retainAll 看似省事,但隐患多于便利
- 如果元素类型是
String或基本包装类,不用额外处理;自定义类必须重写equals()和hashCode()
示例:
Set<String> setB = new HashSet<>(listB);
List<String> intersection = listA.stream()
.filter(setB::contains)
.collect(Collectors.toList());
retainAll 在什么情况下能用,且相对安全
只有当你明确接受“修改原列表”这个副作用,并且该列表是可变、非共享、生命周期短的临时容器时,retainAll 才算合理。
典型场景:解析配置后临时生成的待处理 ID 列表,筛选出数据库中真实存在的那批,筛完立刻丢弃原列表。
- 必须是
ArrayList或LinkedList实例,不能是Collections.unmodifiableList()包装过的 - 调用前建议加注释说明“此处副作用为预期行为”,避免后续维护者误以为是 bug
- 如果
listB很大,先把它转成HashSet再传入,否则retainAll内部会反复调用listB.contains(),退化成 O(n×m)
示例:
List<Integer> tempIds = new ArrayList<>(rawIds); Set<Integer> validIds = queryValidIdsFromDb(); // 返回 Set tempIds.retainAll(validIds); // 这里改的是 tempIds,且 validIds 是 Set,效率有保障
别忽略 null 和重复元素带来的干扰
retainAll 和基于 Stream 的交集逻辑都默认忽略 null 元素——除非你的集合里真存了 null,而 HashSet 允许一个 null,ArrayList 允许多个,这会导致交集结果不一致。
另一个常被忽略的点:交集结果是否允许重复?retainAll 保留左列表中所有“在右列表出现过”的元素,包括重复项;而用 Stream + Set 过滤默认去重(因为 setB::contains 不关心次数)。
- 如果业务要求“listA 中每个重复 ID 出现几次,交集中就保留几次”,就得用
retainAll(且确保listB是Set) - 如果只要“哪些 ID 同时存在”,用
Stream+Set更干净,也更符合数学交集语义 - 任何情况下,都别让
null混进参与交集运算的集合,除非你完整测试过所有分支
retainAll 的破坏性、null 处理、重复语义、以及自定义对象的 equals 实现——这几个点一碰就容易出错,而且错得不明显。










