增强 for(for-each)在修改集合时到底能不能用?
增强 for(for-each)在修改集合时到底能不能用?
增强for循环为什么会 ConcurrentModificationException (CME)、以及正确改法讲清楚。看完你能判断:什么时候能改、怎么改才安全、什么时候会炸。
1)增强 for 是什么?——编译器“反糖”成迭代器
你写的:
for (E e : list) {
body(e);
}
编译后等价于(核心):
for (Iterator<E> it = list.iterator(); it.hasNext();) {
E e = it.next();
body(e);
}
结论:增强 for 一定使用 Iterator 来遍历。这也是触发 fail-fast 的根源。
2)为什么遍历时“直接改 List”会 CME?
以 ArrayList 为例,容器里有个结构性修改计数 modCount;迭代器在创建时把它拷贝为 expectedModCount。每次 next() / hasNext() 都会做:
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
当你在增强 for 里绕过迭代器直接改集合(list.add/remove/clear)时,调用的是list的add/remove/clear而不是迭代器的add/remove/clear所以会发生如下情况:
-
容器的
modCount变了, -
迭代器手里的
expectedModCount没变, -
下一次
hasNext()/next()就 CME。
典型“会炸”的代码
List<Integer> list = new ArrayList<>(List.of(1,2,3));
for (Integer x : list) {
if (x == 2) list.remove(x); // 结构性修改:modCount++,迭代器不知情 → 下一步CME
}
注:
x == 2是引用比较,整数缓存区间(-128~127)里“看着像对”,更稳妥写法是Integer.valueOf(2).equals(x)。
时序图
以下是两张高兼容 Mermaid 图,完整呈现“增强 for → 反糖 → CME 检测”的流程:
-
图 1:时序图(含两条分支:直接改 List → 抛 CME;用迭代器改 → 安全)
-
图 2:决策流(一眼看懂什么时候会 CME)
图 1|增强 for → 反糖 → 迭代检查(含两种修改分支)
图 2|修改方式 → 是否 CME 的决策流
关键点一览
-
反糖:增强 for 一定变成
Iterator循环(list.iterator()→hasNext()→next())。 -
fail-fast:迭代器在
next()/hasNext()前做checkForComodification,比较modCount与expectedModCount。 -
会 CME 的情况:遍历中绕过迭代器做结构性修改(
list.add/remove/clear等)→modCount变了,迭代器快照没变 → 抛ConcurrentModificationException。 -
安全修改:用迭代器自己的方法(
iterator.remove(),或ListIterator.add/set/remove),这些方法会在成功后把expectedModCount同步到最新。 -
仅改值:
list.set(i, v)不改变结构,通常不触发 CME,但要注意你的业务语义是否合理。
3)那为什么“索引 for”通常不报错?
for (int i = 0; i < list.size(); i++) {
if (Integer.valueOf(2).equals(list.get(i))) list.add(90);
}
-
这里 没用迭代器,自然也没有 fail-fast 检查,所以一般不会抛 CME。
-
但逻辑风险很大:
size()在变,可能 越跑越长、跳元素或重复处理。删除时尤其容易“跳过”元素(因为左移)。
4)遍历时“可以改”的正确方式
✅ 用迭代器自己的方法(推荐)
-
Iterator.remove():删除刚由next()返回的那个元素 -
ListIterator.add(E):在当前位置插入 -
ListIterator.set(E):替换最近返回的元素值(非结构性修改)
它们会在内部同步 expectedModCount,不触发 CME。
ListIterator<Integer> it = list.listIterator();
while (it.hasNext()) {
Integer x = it.next();
if (Integer.valueOf(2).equals(x)) {
it.remove(); // 或 it.set(99) / it.add(90)
}
}
✅ 只改“值”不改结构
for (int i = 0; i < list.size(); i++) {
list.set(i, transform(list.get(i))); // 不改大小,通常安全
}
✅ 倒序删除(索引 for)
for (int i = list.size() - 1; i >= 0; i--) {
if (needRemove(list.get(i))) list.remove(i); // 不会跳元素
}
5)增强 for 的“安全与不安全”一览
| 操作 | 在增强 for 中 |
|---|---|
it.remove() / listIterator.add() / listIterator.set() |
✅ 安全(同步快照) |
list.set(i, v)(仅改值) |
✅ 一般安全(不改结构) |
list.add(e) / list.remove(i) / list.clear() |
❌ CME(结构性修改) |
其他线程并发修改同一个 ArrayList |
❌ CME(或更糟) |
6)顺带:Map 的增强 for 也一样
遍历 map.entrySet() 同理,增强 for 底层用迭代器:
for (Iterator<Map.Entry<K,V>> it = map.entrySet().iterator(); it.hasNext();) {
Map.Entry<K,V> e = it.next();
if (needRemove(e)) it.remove(); // 用迭代器 remove
}
直接 map.remove(key) → CME。
7)并发场景的替代方案
-
CopyOnWriteArrayList:读多写少、每次写复制数组;迭代器弱一致,不会 CME,但写入代价高。
-
外部同步:
Collections.synchronizedList(new ArrayList<>())或显式锁,确保“遍历与修改”互斥。 -
批处理:先收集要改的索引/元素,遍历结束后统一修改。
8)实战清单(记住就够用)
-
增强 for 一定是迭代器 → 结构性修改请用 迭代器自带方法。
-
只改值(不改大小)一般没事,但要保证逻辑正确。
-
索引 for 不会 CME,但要防 边界变化/元素搬移 的逻辑坑。
-
并发就别用
ArrayList直接改,考虑并发容器或外部同步。 -
装箱类型比较用
equals,不要依赖==的缓存巧合。
浙公网安备 33010602011771号