主题
界面
主题
for-each 看起来是最简单的语法,但它有两个隐藏问题:
本节把 ConcurrentModificationException 讲透,并给出三种正确的删除写法。
完成本节后,你应该能够:
for-each 编译后等价于什么;ConcurrentModificationException 的真实成因(不是"并发");ListIterator 双向遍历与修改元素。Iterator 吗?(不了解也没关系,本节从零讲)看一段"最自然"的代码:
List<String> items = new ArrayList<>(List.of("a", "b", "c"));
for (String item : items) {
if ("a".equals(item)) {
items.remove(item); // ConcurrentModificationException
}
}几乎所有人第一次写"遍历中删除"都会这么写,然后得到一个名字里带"Concurrent"(并发)的异常——但代码明明是单线程的。这个名字会让人误以为是并发问题,从而找错方向。
理解它,你才能:正确修复、正确解释、以及在面试时答对"为什么删倒数第二个元素不报错"这类问题。
// 你写的
for (String item : items) {
System.out.println(item);
}
// 编译后(等价)
for (Iterator<String> it = items.iterator(); it.hasNext(); ) {
String item = it.next();
System.out.println(item);
}for-each 只是迭代器的语法糖。这个认知解释了后面所有现象——异常的抛出者是迭代器,不是 for-each 本身。
Iterator 接口只有三个方法:
| 方法 | 作用 |
|---|---|
hasNext() | 是否还有下一个 |
next() | 取出下一个元素 |
remove() | 删除刚取出的那个元素 |
它和"并发"没有关系,是"遍历期间集合被修改":
① 创建迭代器时,记录当前"修改次数"(modCount)
② 每次调用 next() 时,检查 modCount 是否还是创建时的值
③ 如果在遍历过程中集合被 add/remove(modCount 变了)
→ 抛 ConcurrentModificationException这个机制叫 fail-fast(快速失败):集合宁可立刻报错,也不返回不可预期的结果。ArrayList、HashMap、HashSet 都是 fail-fast。
为什么会"偶发"
这正是坑所在:并不是"一修改就抛"。
next() 之后(循环马上结束),就不会再调用 next(),也就没机会检查 → 不抛异常;next() → 抛异常。所以同一个 bug 可能"有时候报错、有时候不报",取决于你要删的是第几个元素。
List<String> items = new ArrayList<>(List.of("a", "b", "c"));
for (String item : items) {
if ("b".equals(item)) { // b 是倒数第二个
items.remove(item);
}
}
System.out.println(items); // [a, c] —— 没有抛异常!过程分析:
迭代器内部有一个游标 cursor:
取 "a" → cursor = 1
取 "b" → cursor = 2,然后 remove("b"),此时 size 从 3 变成 2
判断 hasNext():cursor(2) != size(2) → false → 循环结束
↑ 关键:没有再调用 next(),所以没有触发检查删第一个元素会立刻抛异常(cursor = 1,size = 2,还会继续 next());删倒数第二个则"恰好"跳过检查。
危险在哪:这段代码在数据量变化时(比如列表变长)就会突然开始抛异常。"能跑通"不等于"写对了"。
// 方式一:removeIf(推荐,最简洁)
items.removeIf("b"::equals);
// 方式二:显式迭代器的 remove
Iterator<String> iterator = items.iterator();
while (iterator.hasNext()) {
if ("b".equals(iterator.next())) {
iterator.remove();
}
}
// 方式三:倒序下标循环(ArrayList 友好)
for (int i = items.size() - 1; i >= 0; i--) {
if ("b".equals(items.get(i))) {
items.remove(i);
}
}| 方式 | 代码量 | 适用场景 |
|---|---|---|
removeIf | 最少 | 首选,条件简单时 |
| 迭代器 | 中 | 需要在删除时做额外逻辑,或遍历 Set |
| 倒序循环 | 中 | 需要按索引处理,或对 ArrayList 做批量删除 |
为什么倒序不报错
普通的下标循环 for (int i = 0; i < size; i++) 在删除元素后会跳过下一个元素(因为后面的元素前移了);倒序遍历则不受影响。
而且下标循环不使用迭代器,所以不会触发 fail-fast 检查——这是"绕过"而不是"修复",只在 ArrayList 上语义清晰。
ListIterator 比 Iterator 多出四个能力:双向遍历、set 修改当前元素、add 插入元素、通过 nextIndex / previousIndex 拿到下标。
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
String value = it.next();
it.set(value.toUpperCase()); // 修改当前元素(安全)
}注意:只有 List 有 ListIterator,Set 没有(因为无序,谈不上位置)。
上面的方式都是单线程下的正确写法。多线程并发修改时,即使使用迭代器的 remove 也不安全。
| 场景 | 方案 |
|---|---|
| 多线程读多写少 | CopyOnWriteArrayList(fail-safe,遍历时看到的是快照) |
| 多线程读写均衡 | Collections.synchronizedList(...) + 外部同步遍历 |
| 高并发 | ConcurrentHashMap 等并发集合(《JUC 工具类与并发容器》) |
注意:CopyOnWriteArrayList 遍历时不会抛异常,但也读不到遍历期间的新增元素——这是"快照"语义,不是 bug。
Iterator<String> iterator = items.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}// 需求:删除所有以 "temp" 开头的元素
items.removeIf(item -> item.startsWith("temp"));removeIf 是首选:一行、语义清晰、内部实现高效(ArrayList 里做一次搬移而不是多次)。
// 错误一:for-each 里直接 remove(取决于删除位置,可能抛异常)
for (String item : items) {
items.remove(item);
}
// 错误二:普通下标循环里删除(漏掉元素)
for (int i = 0; i < items.size(); i++) {
if (需要删除) {
items.remove(i); // i 后面的元素前移,i 会跳过下一个
}
}
// 错误三:用另一个集合收集后再删(能跑,但多一次遍历与一份内存)
List<String> toRemove = new ArrayList<>();
for (String item : items) {
if (需要删除) {
toRemove.add(item);
}
}
items.removeAll(toRemove);错误二最容易产生"漏删"的静默 bug——不报错,只是结果不对。
文件:labs/collections/src/main/java/cn/kb/learn/collection/IteratorLesson.java
package cn.kb.learn.collection;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
public class IteratorLesson {
public static void main(String[] args) {
// 1. 显式使用迭代器
List<String> items = new ArrayList<>(List.of("a", "b", "c", "d"));
Iterator<String> iterator = items.iterator();
StringBuilder visited = new StringBuilder();
while (iterator.hasNext()) {
visited.append(iterator.next()).append(' ');
}
System.out.println("显式迭代器遍历: " + visited.toString().trim());
// 2. for-each 是迭代器的语法糖
StringBuilder forEach = new StringBuilder();
for (String item : items) {
forEach.append(item).append(' ');
}
System.out.println("for-each 遍历: " + forEach.toString().trim());
// 3. 遍历中删除的三种正确方式
List<String> byRemoveIf = new ArrayList<>(List.of("a", "b", "c"));
byRemoveIf.removeIf("b"::equals);
System.out.println("removeIf: " + byRemoveIf);
List<String> byIterator = new ArrayList<>(List.of("a", "b", "c"));
Iterator<String> removing = byIterator.iterator();
while (removing.hasNext()) {
if ("b".equals(removing.next())) {
removing.remove();
}
}
System.out.println("iterator.remove: " + byIterator);
List<String> byReverse = new ArrayList<>(List.of("a", "b", "c"));
for (int i = byReverse.size() - 1; i >= 0; i--) {
if ("b".equals(byReverse.get(i))) {
byReverse.remove(i);
}
}
System.out.println("倒序下标循环: " + byReverse);
// 4. 错误方式:for-each 中直接删除
List<String> broken = new ArrayList<>(List.of("a", "b", "c"));
try {
for (String item : broken) {
if ("a".equals(item)) {
broken.remove(item);
}
}
} catch (ConcurrentModificationException exception) {
System.out.println("for-each 中直接删除: ConcurrentModificationException");
}
// 5. 删倒数第二个元素:不抛异常,但同样是错的
List<String> lucky = new ArrayList<>(List.of("a", "b", "c"));
for (String item : lucky) {
if ("b".equals(item)) {
lucky.remove(item);
}
}
System.out.println("删倒数第二个(侥幸通过): " + lucky);
// 6. ListIterator:可双向遍历、可修改元素
List<String> editable = new ArrayList<>(List.of("a", "b", "c"));
ListIterator<String> listIterator = editable.listIterator();
StringBuilder changed = new StringBuilder();
while (listIterator.hasNext()) {
int index = listIterator.nextIndex();
String value = listIterator.next();
listIterator.set(value.toUpperCase());
changed.append("下标 ").append(index).append(" -> ").append(value.toUpperCase()).append(" | ");
}
System.out.println("ListIterator 修改过程: " + changed.toString().trim());
System.out.println("修改后: " + editable);
}
}运行命令:
cd labs
./mvnw -pl collections compile
java -cp collections/target/classes cn.kb.learn.collection.IteratorLesson预期输出:
显式迭代器遍历: a b c d
for-each 遍历: a b c d
removeIf: [a, c]
iterator.remove: [a, c]
倒序下标循环: [a, c]
for-each 中直接删除: ConcurrentModificationException
删倒数第二个(侥幸通过): [a, c]
ListIterator 修改过程: 下标 0 -> A | 下标 1 -> B | 下标 2 -> C |
修改后: [A, B, C]第 6 行与第 7 行的对比是本节的核心:
"a"(第一个)→ 抛 ConcurrentModificationException;"b"(倒数第二个)→ 静默通过。两者都是错误写法,但只有前者会提醒你。这就是为什么这个 bug 在生产环境里往往"突然出现"——当数据量或删除位置变化时,它就暴露了。
broken.remove(item)(删第一个)处断点,F7 进入 ArrayList.remove → 观察 modCount++;继续按 F9,观察 Itr.next() 里的 checkForComodification() 抛异常;hasNext() 返回 false 后正常结束——没有机会调用 next();iterator.remove() 处 F7 进入,观察它同时更新了 expectedModCount——这是它与"直接调 list.remove"的关键区别;ArrayList 的迭代器对象,看 cursor 与 expectedModCount 两个字段;对比直接 remove 后 modCount 与 expectedModCount 不再相等。见 6.3 节。修复:removeIf / 迭代器 / 倒序循环。
List<Integer> list = new ArrayList<>(List.of(1, 2, 2, 3));
for (int i = 0; i < list.size(); i++) {
if (list.get(i) == 2) {
list.remove(i); // 删除后 i 自增,跳过了下一个 2
}
}
System.out.println(list); // [1, 2, 3] —— 还有一个 2 没删掉修复:倒序遍历,或删除后不递增下标(i--)。
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue() == 0) {
map.remove(entry.getKey()); // ConcurrentModificationException
}
}修复:
map.entrySet().removeIf(entry -> entry.getValue() == 0); // 推荐
// 或
map.entrySet().stream().filter(...).toList().forEach(map::remove); // 先收集再删CopyOnWriteArrayList 能读到最新数据 CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (String item : list) {
list.add("new"); // 不抛异常,但本次遍历看不到 "new"
}这不是 bug,是"快照"语义(《JUC 工具类与并发容器》 会展开)。
目标:亲手验证 fail-fast 的触发条件。
步骤:
list.removeIf(...) 与"另一个集合收集后 removeAll"两种方式在 10 万条数据上的耗时;ListIterator 实现"把所有偶数元素替换成 0";Map + entrySet().removeIf(...) 删除特定条件的条目,验证不抛异常。验收方式:能解释第 2 步的结果(提示:删最后一个元素后 hasNext() 也返回 false)。
扩展任务:写一个方法 List<String> removeAllSafely(List<String> list, Predicate<String> condition),要求不修改传入的集合而是返回新列表,说明这种"不改参数"的风格为什么更安全(回顾 《方法》 的"值传递"与 《封装》 的"最小副作用")。
基础题
ConcurrentModificationException 只在多线程环境下出现。(对 / 错)for-each 编译后等价于 ______。iterator.remove() 不会抛异常,而 list.remove() 会?代码阅读题
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
for (Integer value : list) {
if (value % 2 == 0) {
list.remove(value);
}
}
System.out.println(list);编程题
List<String> filterOut(List<String> source, Predicate<String> condition): removeIf 实现一次,对比代码。综合任务
List<CartItem>(sku、quantity、price);removeIf(假设后续要在删除时记录日志),用迭代器实现;remove?提示 1(方向):第 4 题的删除发生在第二个元素上,后面还有元素,所以会抛。
提示 2(关键点):第 5 题的迭代器版本要"新建一个列表放保留的元素",或者对副本用迭代器删除——不要修改入参。
提示 3(接近答案):第 6 题用迭代器遍历时,可以在 iterator.remove() 前后加日志,因为 remove 是通过迭代器执行的,不会破坏遍历。
removeIf、for (Iterator it = c.iterator(); it.hasNext(); )(迭代器循环)。iterator.remove() 在删除元素后会同步更新迭代器内部的 expectedModCount,使它与集合的 modCount 保持一致;而直接调 list.remove() 只增加 modCount,迭代器下次 next() 时检查发现不一致,于是抛异常。会抛异常,在删除元素 2 时抛出。
过程:
list.remove(2),此时 modCount 增加;next() 取下一个元素 → checkForComodification() 发现不一致 → 抛 ConcurrentModificationException。如果只删最后一个偶数(比如列表是 [1, 2]),删除后 hasNext() 返回 false,不会再调用 next(),就不会抛异常——这再次说明"报不报错取决于位置"。
package cn.kb.learn.collection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Predicate;
public class ListFilter {
/** 迭代器版本:不修改入参 */
static List<String> filterOutByIterator(List<String> source, Predicate<String> condition) {
List<String> result = new ArrayList<>();
if (source == null) {
return result;
}
Iterator<String> iterator = source.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (!condition.test(item)) {
result.add(item);
}
}
return result;
}
/** removeIf 版本:对副本操作,同样不修改入参 */
static List<String> filterOutByRemoveIf(List<String> source, Predicate<String> condition) {
List<String> copy = new ArrayList<>(source == null ? List.of() : source);
copy.removeIf(condition);
return copy;
}
public static void main(String[] args) {
List<String> source = List.of("apple", "banana", "cherry");
System.out.println(filterOutByIterator(source, item -> item.startsWith("b")));
System.out.println(filterOutByRemoveIf(source, item -> item.startsWith("b")));
System.out.println("原列表未被修改: " + source);
}
}代码对比:removeIf 版本只有两行(复制 + 删除),迭代器版本要写五行的循环。但 removeIf 版本多了一次全量复制——数据量极大时,迭代器版本(一次遍历、按需添加)更省内存。
预期输出:
[apple, cherry]
[apple, cherry]
原列表未被修改: [apple, banana, cherry]package cn.kb.learn.collection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CartCleaner {
record CartItem(String sku, int quantity, long priceInCents) {
CartItem withQuantity(int newQuantity) {
return new CartItem(sku, newQuantity, priceInCents);
}
}
static List<String> clean(List<CartItem> items, java.util.Map<String, Integer> stock, List<String> logs) {
List<String> pendingRestock = new ArrayList<>();
Iterator<CartItem> iterator = items.iterator();
while (iterator.hasNext()) {
CartItem item = iterator.next();
if (item.quantity() <= 0 || item.priceInCents() < 0) {
logs.add("移除无效条目: " + item.sku());
iterator.remove(); // 通过迭代器删除,安全
continue;
}
int available = stock.getOrDefault(item.sku(), 0);
if (available < item.quantity()) {
pendingRestock.add(item.sku());
logs.add("库存不足,标记待补货: " + item.sku());
}
}
return pendingRestock;
}
public static void main(String[] args) {
List<CartItem> items = new ArrayList<>(List.of(
new CartItem("A", 2, 1000),
new CartItem("B", 0, 500),
new CartItem("C", 5, 2000)));
java.util.Map<String, Integer> stock = java.util.Map.of("A", 10, "C", 1);
List<String> logs = new ArrayList<>();
List<String> pendingRestock = clean(items, stock, logs);
System.out.println("剩余条目: " + items);
System.out.println("待补货: " + pendingRestock);
logs.forEach(log -> System.out.println(" 日志: " + log));
}
}为什么不能在 for-each 里直接 remove:for-each 隐藏了迭代器,你拿到的是元素本身,没有删除的合法途径。要么用 removeIf(不需要在删除时做额外动作),要么显式使用迭代器(可以在 remove 前后记录日志、做业务判断)。
这个例子里"删除 + 记录日志"同时发生,所以必须用显式迭代器——removeIf 做不到"删除时执行副作用"。
预期输出:
剩余条目: [CartItem[sku=A, quantity=2, priceInCents=1000], CartItem[sku=C, quantity=5, priceInCents=2000]]
待补货: [C]
日志: 移除无效条目: B
日志: 库存不足,标记待补货: Cfor-each 编译后等价于迭代器循环;ConcurrentModificationException 的成因与 fail-fast 机制;ListIterator 做双向遍历与元素修改。集合模块完成(S1-015 ~ S1-019)。《泛型基础》:集合为什么能"记住"元素类型,以及 <T> 到底做了什么。
暂无