很多同学在「遍历集合并删除元素」时遇到过 ConcurrentModificationException,它字面意思是「并发修改」,但很多时候其实并没有多线程。这篇文章把这个坑讲清楚。

一、问题复现

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");

for (String item : list) {
    if ("b".equals(item)) {
        list.remove(item); // 触发 ConcurrentModificationException
    }
}

运行这段代码,会在 list.remove 后再次取下一个元素时报错。这是因为增强 for 底层使用的是 Iterator。

二、为什么会有 fail-fast

以 ArrayList 为例,它内部维护一个 modCount(修改次数)计数器。每次结构性修改(add / remove 等)都会让 modCount 加一。迭代器在创建时保存了一份 expectedModCount,每次 next() 时校验:

if (modCount != expectedModCount) {
    throw new ConcurrentModificationException();
}

直接调用 list.remove(item) 会让 modCount 变化,而迭代器内的 expectedModCount 没有同步更新,于是异常发生。这个设计叫 fail-fast 机制,目的是尽早暴露错误,避免在错误状态下继续执行。

三、正确的删除方式

1. 使用迭代器的 remove 方法

迭代器自己的 remove() 会同步更新 expectedModCount,所以是安全的:

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if ("b".equals(it.next())) {
        it.remove(); // 安全
    }
}

2. Java 8 的 removeIf(推荐)

list.removeIf(item -> "b".equals(item));

3. 先用收集再统一删除

List<String> toRemove = new ArrayList<>();
for (String item : list) {
    if ("b".equals(item)) {
        toRemove.add(item);
    }
}
list.removeAll(toRemove);

4. 使用 CopyOnWriteArrayList(并发场景)

在真正的并发环境里也可以选择 CopyOnWriteArrayList,它的迭代器基于快照,遍历时修改不会抛异常:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 写入时复制,遍历时删除不会抛 ConcurrentModificationException

四、小结

  • 增强 for 循环内部用的是迭代器,直接调用集合的 remove 会触发 fail-fast。
  • 单线程内推荐 removeIf 或迭代器 remove()。
  • 多线程修改同一集合时,优先考虑 CopyOnWriteArrayList 等线程安全集合,而不是靠 try-catch 掩盖问题。