retainall通过原地覆盖+尾部截断实现交集,不移动元素仅重排前端并置空冗余位;依赖c.contains()性能,要求equals正确实现,非线程安全。

retainAll 并不真正“移动数组元素”来求交集,而是通过标记保留 + 批量复制的方式实现,核心是原地覆盖 + 尾部截断。
retainAll 的实际执行逻辑
调用 retainAll(Collection> c) 时,ArrayList 并不会新建数组或逐个搬移元素,而是:
- 遍历当前 list 的每个元素(按索引从 0 到 size-1);
- 对每个元素判断是否在参数集合
c中存在(调用c.contains(e)); - 若存在,将其“保留在前段”,用一个写入指针
w记录下一个应存放位置; - 若不存在,跳过,该位置后续会被覆盖或丢弃;
- 遍历完成后,将
size设为w,并把多余元素置为null(便于 GC)。
底层数组如何“被操作”
数组本身没有移动,但数据被重新紧凑排列在原数组前端:
- 初始数组:[A, B, C, D, E],size=5;
- 假设
c = [A, C, E],遍历时:A→保留(w=0),B→丢弃,C→保留(w=1),D→丢弃,E→保留(w=2); - 最终数组变成:[A, C, E, D, E](后两个位置未清空前仍残留旧值);
- 接着执行
Arrays.fill(elementData, w, size, null),即把索引 3~4 置 null; - 最后设
size = w = 3,对外表现为 [A, C, E]。
关键细节与注意事项
这个过程依赖几个隐含前提:
-
c.contains()的时间复杂度直接影响整体性能——建议传入 HashSet 等 O(1) 查找的集合; - 元素是否保留完全取决于
equals()判断,注意重写规则; - 原数组长度不变,仅逻辑 size 缩小,内存不会立即释放;
- 不是线程安全操作,多线程环境下需同步。
对比 removeIf 或手动遍历
retainAll 和 removeIf(e -> !c.contains(e)) 行为一致,但 retainAll 是标准语义封装;手动倒序遍历删除虽可行,但涉及多次数组复制(remove(int) 会移动后续元素),效率更低。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











