什么原因会导致ConcurrentModificationException异常?
嘿,这个问题问得特别实在!ConcurrentModificationException(咱们简称CME)是Java集合里最容易踩的坑之一,刚好你贴的这段就是ArrayList内部迭代器Itr的核心代码,咱们一步步拆解清楚。
一、为什么会触发ConcurrentModificationException?
核心原因就俩变量:modCount和expectedModCount的不一致。
modCount是ArrayList类内部的成员变量,用来记录集合被修改的总次数——只要你调用add、remove、clear这类会改变集合结构的方法,它就会自增1。expectedModCount是迭代器Itr的成员变量,在你调用list.iterator()创建迭代器的时候,它会直接复制当前ArrayList的modCount值。
而你代码里的checkForComodification()方法,就是迭代器的"守门人":
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
每次调用迭代器的next()、remove()方法前,都会先执行这个检查——只要发现modCount和expectedModCount不一样,直接抛出CME,告诉你"集合被偷偷修改了!"
常见触发场景有两种:
- 单线程踩坑:用迭代器遍历集合的同时,直接调用
ArrayList自己的add/remove方法(不是迭代器的remove)。比如:
这时候下一次调用ArrayList<String> list = new ArrayList<>(List.of("a", "b", "c")); Iterator<String> it = list.iterator(); while (it.hasNext()) { String item = it.next(); if (item.equals("b")) { list.remove(item); // 直接调用list的remove,modCount+1,但expectedModCount没更新 } }it.next()时,检查就会失败,抛出CME。 - 多线程并发:一个线程用迭代器遍历,另一个线程修改集合。因为
modCount不是线程安全的,两个线程操作后很容易出现modCount和expectedModCount不一致的情况。
二、你贴的这段迭代器代码逻辑合理吗?
太合理了!这是Java集合"快速失败(fail-fast)"机制的典型实现,每一步都在守护迭代的安全性:
1. hasNext()方法
public boolean hasNext() { return cursor != size; }
逻辑极简:cursor是迭代器当前指向的下一个元素的索引,只要它不等于集合的size,就说明还有元素可以遍历。完全符合直觉,没有多余操作。
2. next()方法
@SuppressWarnings("unchecked") public E next() { checkForComodification(); // 先检查,确保集合没被外部修改 int i = cursor; if (i >= size) throw new NoSuchElementException(); // 没元素了还调用next?抛异常 Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); // 防御性检查:正常情况下cursor不会超过elementData长度,除非集合被外部非法修改 cursor = i + 1; // 移动cursor到下一个位置 return (E) elementData[lastRet = i]; // 记录lastRet(最后返回的元素索引),然后返回元素 }
这里的每一步都有意义:
- 先做检查,把非法修改扼杀在摇篮里;
- 两次越界判断,分别处理"遍历完了还调用next"和"集合被外部非法修改导致结构混乱"的情况;
- 移动cursor并记录lastRet,为后续的
remove()操作做准备。
3. remove()方法
public void remove() { if (lastRet < 0) throw new IllegalStateException(); // 还没调用next就想remove?不行 checkForComodification(); // 先检查,确保调用remove前集合没被外部修改 try { ArrayList.this.remove(lastRet); // 调用ArrayList的remove方法删除元素 cursor = lastRet; // 因为删除元素后,后面的元素前移了,cursor要回退一位,避免跳过下一个元素 lastRet = -1; // 标记lastRet无效,防止重复调用remove expectedModCount = modCount; // 关键!同步expectedModCount和modCount,因为这次是迭代器自己发起的修改,合法,所以不会触发后续检查失败 } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); // 把索引异常包装成CME,统一异常类型 } }
这是迭代器最巧妙的地方:只有通过迭代器自己的remove()方法修改集合,才会同步expectedModCount,所以不会触发CME。这也是为什么我们遍历集合要删除元素时,必须用迭代器的remove()而不是集合自己的remove()。
4. checkForComodification()方法
这个方法就是快速失败机制的核心:它不允许在迭代过程中,集合被迭代器之外的操作修改。一旦发现,立刻抛出异常,避免程序继续运行出现更隐蔽的错误(比如遍历到错误的元素、数组越界、数据不一致等)。
总结
这段迭代器代码的设计非常严谨,完全围绕"快速失败"原则:在不安全的集合修改操作发生时,尽早抛出异常,提醒开发者修正代码,而不是让程序带着错误继续运行。这也是Java集合框架保证迭代安全性的重要手段。
内容的提问来源于stack exchange,提问作者Ankush Dutt

