在Java开发中,循环删除List元素是一项常见但容易出错的操作。即便是经验丰富的开发者,也可能不慎触发ConcurrentModificationException异常。本文将从一个典型错误案例入手,深入剖析异常根源,并详细介绍三种安全有效的实现方式。

1. 新手常犯的错误
许多初学者首先会尝试使用foreach循环进行删除,代码如下:
public static void main(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客园");
platformList.add("CSDN");
platformList.add("掘金");
for (String platform : platformList) {
if (platform.equals("博客园")) {
platformList.remove(platform);
}
}
System.out.println(platformList);
}
运行这段代码会立即抛出java.util.ConcurrentModificationException异常,即并发修改异常。

为什么会出现这个异常?让我们先查看上述代码编译后的字节码:

从字节码可以清晰地看到,foreach循环在底层是通过Iterator实现的,核心依赖于hasNext()和next()方法。
接下来,我们查看ArrayList中Iterator的源码实现:

异常抛出的关键位置就在next()方法内部:

在next()方法中,第一行代码就调用了checkForComodification()。这个方法的核心是比较modCount和expectedModCount两个变量的值。在初始状态下,两者的值均为3。
但当执行platformList.remove(platform);语句后,modCount的值被修改为4。

因此,当第二次调用next()试图获取元素时,modCount(4)与expectedModCount(3)不再相等,从而触发异常。

既然foreach循环行不通,有哪些方法可以安全地实现循环删除呢?主要有以下三种途径:
- 使用
Iterator的remove()方法
- 使用for循环正序遍历
- 使用for循环倒序遍历
下面我们逐一进行详细解析。
2. 使用Iterator的remove()方法
直接调用Iterator自身的remove()方法是推荐的做法,实现代码如下:
public static void main(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客园");
platformList.add("CSDN");
platformList.add("掘金");
Iterator<String> iterator = platformList.iterator();
while (iterator.hasNext()) {
String platform = iterator.next();
if (platform.equals("博客园")) {
iterator.remove();
}
}
System.out.println(platformList);
}
运行后输出结果为:
[CSDN, 掘金]
为什么iterator.remove()不会引发异常?关键在于其源码实现:

每次成功删除一个元素后,该方法都会将当前的modCount值重新赋给expectedModCount,使得两者始终保持一致,从而顺利通过checkForComodification()检查。
3. 使用for循环正序遍历
通过传统的for循环和下标操作也能实现删除,但需要特别注意下标调整:
public static void main(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客园");
platformList.add("CSDN");
platformList.add("掘金");
for (int i = 0; i < platformList.size(); i++) {
String item = platformList.get(i);
if (item.equals("博客园")) {
platformList.remove(i);
i = i - 1;
}
}
System.out.println(platformList);
}
这段代码有一个关键点:在删除元素后,需要执行i = i - 1;来修正下标。原因可以通过图示理解。
删除前,列表元素的下标分布如下:

当删除“博客园”(下标0)后,剩余元素会前移,下标变为:

如果不将i减1,下一次循环时i的值为1,将直接访问“掘金”,导致“CSDN”被遗漏。因此,修正下标是确保每个元素都被检查的必要步骤。
4. 使用for循环倒序遍历
倒序遍历是另一种有效方式,它天然避免了下标修正的问题:
public static void main(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客园");
platformList.add("CSDN");
platformList.add("掘金");
for (int i = platformList.size() - 1; i >= 0; i--) {
String item = platformList.get(i);
if (item.equals("掘金")) {
platformList.remove(i);
}
}
System.out.println(platformList);
}
初始下标状态与正序示例相同。删除“掘金”(下标2)后,列表状态变为:

由于遍历方向是从后向前,元素的删除不会影响尚未遍历到的索引位置,因此无需任何额外调整。
5. 使用Stream filter过滤
对于Java 8及以上版本,利用Stream API进行过滤是一种更函数式、更简洁的方案:
public static void main(String[] args) {
List<String> platformList = new ArrayList();
platformList.add("博客园");
platformList.add("CSDN");
platformList.add("掘金");
platformList = platformList.stream().filter(e -> !e.startsWith("博客园")).collect(Collectors.toList());
System.out.println(platformList);
}
运行结果如下:

这种方法并非直接修改原列表,而是通过流式操作筛选出符合条件的元素并收集到一个新列表中。代码意图清晰,且完全避免了并发修改异常。
总结来说,在Java中循环删除List元素时,应避免直接在使用foreach循环中操作原列表。根据具体场景,灵活选用Iterator.remove()、手动管理下标的for循环或Stream API,可以既安全又高效地完成任务。