很多同学在「遍历集合并删除元素」时遇到过 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 掩盖问题。