retainall() 是原地过滤操作而非纯交集函数:调用 a.retainall(b) 后 a 变为 a ∩ b,b 不变;返回 boolean 表示是否删除元素,不能用于判断交集存在;性能上 arraylist 版为 o(m×n),应将参数转 hashset 优化;hashset 版接近 o(m),更符合集合交集语义。

retainAll() 不是“求交集函数”,而是“原地过滤”操作
它不会返回新集合,而是直接修改调用方——A.retainAll(B) 后,A 变成 A ∩ B,B 完全不变。这点常被误当成纯函数用,结果在循环里反复调用导致数据意外丢失。
- 如果后续还要用原
A,必须提前拷贝:new ArrayList(A).retainAll(B) - 若
A是HashSet,交集后元素顺序无意义;若是ArrayList,则保留原始出现顺序(比如[5, 1, 3]∩[3, 1, 9]→[1, 3],不是[3, 1]) - 返回值是
boolean:只要删了至少一个元素就返回true,哪怕最后只剩一个元素;完全没动也返回false——不能靠它判断“有没有交集”,得看A.isEmpty()或A.size() > 0
ArrayList.retainAll() 性能差的根源在双重遍历 + remove开销
对 ArrayList 调用 retainAll(),JDK 内部会逐个调用 c.contains(e) 判断是否保留,再执行 remove()。而 remove() 是 O(n) 操作,每次删都要搬动后面所有元素;如果参数 c 是另一个 ArrayList,c.contains() 又是 O(n) 线性扫描——合起来就是 O(m × n),十万级数据可能卡顿数秒。
- 别让
c是ArrayList或LinkedList,务必转成HashSet:new HashSet(c) - 自己实现等效逻辑时,优先用
stream().filter(setC::contains).collect(...),语义清、不改原集合、还能并行 - 真要原地改且量大?先转
HashSet存A,做完交集再转回ArrayList(但注意丢顺序)
HashSet.retainAll() 才是真正接近 O(n) 的交集操作
HashSet.retainAll() 底层是哈希定位删除,每个元素查 c 是 O(1) 平均复杂度,删本身也是 O(1),整体趋近 O(m)。而且它天然去重、无视顺序,数学意义上更贴近“集合交集”本意。
- 如果你只关心“哪些元素共同存在”,别用
ArrayList存中间结果,从源头就用HashSet - 权限校验、标签匹配、ID 过滤等场景,
Set+retainAll()是更稳的选择 - 注意:如果
c是空集合,retainAll()会清空整个调用方——这是合法行为,不是 bug
判断“是否有交集”别用 retainAll(),除非你真想改原集合
很多人用 list1.retainAll(list2); if (!list1.isEmpty()) {...} 来检测交集,这既改了 list1,又浪费了大量时间做删除动作。其实只需一次遍历 + 快速查找。
- 推荐写法:
list2.stream().collect(Collectors.toSet()); list1.stream().anyMatch(set2::contains) - 更省内存:把小集合转
Set,遍历大集合——谁小谁转,避免构造大哈希表 - 如果连
Stream都不想用,就手动循环:for (E e : smallList) if (bigSet.contains(e)) return true;
retainAll() 是个带副作用的破坏性操作,以及默认用 ArrayList 去扛交集计算这件事本身就不合理。











