foreach中直接调用集合remove会触发ConcurrentModificationException,因迭代器的modCount与expectedModCount不一致;还可能导致元素跳过或重复处理,引发逻辑错误。

在 foreach 循环中直接调用集合的 remove() 方法,不仅会触发 ConcurrentModificationException(并发修改异常),在某些特殊场景下还可能陷入看似“死循环”的假象——实际是循环未按预期退出、元素反复被访问或跳过,导致逻辑错误甚至无限执行。
为什么 foreach + remove 会出问题
Java 的 foreach 底层依赖迭代器(Iterator),而大多数集合(如 ArrayList、HashMap)的迭代器是快速失败(fail-fast)的。当迭代器创建后,若集合结构被外部修改(如 add/remove 元素),但迭代器自身未感知(即未通过 iterator.remove()),就会在下次调用 next() 或 hasNext() 时抛出 ConcurrentModificationException。
但注意:如果在循环中 remove 的是当前正在遍历的元素,且恰好导致后续元素前移、而迭代器的游标(cursor)未同步调整,就可能出现“同一个元素被重复处理”或“跳过下一个元素”的行为——这在逻辑上容易被误认为“死循环”,尤其当业务代码含 continue、条件判断或异常吞并时更隐蔽。
常见错误写法与风险示例
以下代码在运行时大概率抛异常,或产生不可预期行为:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
if ("b".equals(s)) {
list.remove(s); // ❌ 危险!破坏迭代器一致性
}
}
- ArrayList 在 remove("b") 后,"c" 和 "d" 前移,但 foreach 的迭代器仍按原索引推进,导致 "c" 被跳过;
- 若循环体中还有其他 remove 操作(如嵌套判断),可能让迭代器状态持续错位;
- 部分 JDK 版本或特定集合实现(如 CopyOnWriteArrayList)虽不抛异常,但语义完全不同——它每次操作都复制数组,不适合高频修改场景。
安全删除的正确做法
必须使用迭代器自身的 remove() 方法,它能同步维护内部状态:
-
使用显式 Iterator:
Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if ("b".equals(s)) { it.remove(); // ✅ 安全,由迭代器控制结构变更 } } -
使用 removeIf(JDK 8+),语义清晰、线程安全(单线程下):
list.removeIf("b"::equals); -
倒序 for 循环(仅适用于 List),避免索引偏移影响:
for (int i = list.size() - 1; i >= 0; i--) { if ("b".equals(list.get(i))) { list.remove(i); } }
调试这类 Bug 的关键线索
遇到疑似“foreach 中 remove 导致卡住/漏删/重复处理”,优先检查:
- 是否捕获了 ConcurrentModificationException 但未打印日志(静默吞掉异常);
- 循环内是否有多个 remove 调用,或存在 try-catch 包裹部分逻辑;
- 集合是否被多线程共享修改(即使没用 foreach,也要考虑线程安全);
- 是否误用了增强 for 遍历 LinkedList —— 它的迭代器虽也是 fail-fast,但因链表结构特性,异常触发时机可能略有不同,但原则不变。

















