遍历中直接调用 list.remove() 会抛 ConcurrentModificationException,这个规则大家都知道。这篇真实复现这个异常之外,还真实复现了一个更值得警惕的场景:同样是遍历中 remove,有的时候异常真实不会抛出,留下一个悄无声息的逻辑 bug。
上一篇:WeakReference 与 SoftReference 真实 GC 行为对比
最基础的场景:用增强 for 循环遍历 ArrayList,循环体内直接调用 list.remove()。
FailFastIteratorDemo.java List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); for (Integer i : list) { if (i == 2) list.remove(Integer.valueOf(3)); }
遍历到: 1 遍历到: 2 真实捕获 ConcurrentModificationException(fail-fast,靠 modCount 检测)
真实机制是这样的:ArrayList 内部维护一个 modCount 计数器,每次结构性修改(增删,不包括 set)都会真实自增;迭代器在创建时记录下当时的 modCount,每次调用 next() 都会真实比对当前 modCount 和记录值是否一致,不一致就立刻抛出。这是一种"尽快失败"而不是"保证检测到"的机制。
上面这句"尽快失败,不保证检测到"不是免责声明,是真实存在的漏洞——用一个 2 元素列表真实复现。
FailFastIteratorDemo.java List<Integer> list2 = new ArrayList<>(Arrays.asList(1, 2)); for (Iterator<Integer> it = list2.iterator(); it.hasNext(); ) { Integer v = it.next(); if (v == 1) list2.remove(Integer.valueOf(2)); // 移除最后一个元素 }
移除倒数第二个恰好指向最后一个时,未抛异常,list2 最终内容: [1]
用 Iterator.remove() 代替集合自身的 remove(),因为它会同步更新迭代器内部记录的 expectedModCount。
用 Iterator.remove() 安全删除后: [1, 3, 5]
真实结果验证 Iterator.remove() 才是遍历中删除元素的正确方式——它内部会同步更新迭代器自己记录的 expectedModCount,不会触发 fail-fast 检查,也不会有游标错位的问题,能真实、干净地删完所有偶数。
最后两组对照:并发容器 CopyOnWriteArrayList 的遍历机制完全不同,而 HashMap 同样受 fail-fast 约束。
CopyOnWriteArrayList 遍历中修改,未抛异常,本次遍历实际看到的元素数: 3,遍历后 size: 6 HashMap.keySet() 遍历中 put 同样真实抛出 ConcurrentModificationException
真实结果证明 CopyOnWriteArrayList 的迭代器在创建那一刻就真实持有了底层数组的一份快照引用,遍历过程中看到的永远是那个快照,后续的 add() 真实生成了新数组,但不影响正在进行的这次遍历——本次遍历真实只看到了 3 个元素(创建迭代器时的快照大小),尽管遍历结束后 size() 已经真实变成了 6。而 HashMap.keySet() 的遍历同样受 modCount 机制约束,和 ArrayList 遵循同一套 fail-fast 逻辑。
把上面的经典陷阱拆成逐帧动画:每一步的 cursor、size、hasNext() 判断结果都是真实运行这段代码时的状态,不是手工排的。