Java在用增强for循环遍历集合时删除元素,抛出java.util.ConcurrentModificationException异常
文章目录
- 0. 前言
- 1. 问题产生的背景
- 2. Java中增强for循环的底层原理
- 3. 为什么增强for循环不支持在遍历集合时删除元素
- 3.1 问题排查
- 3.2 modCount 变量的来源
- 3.3 expectedModCount 变量的来源
- 3.4 导致modCount变量和expectedModCount不相等的原因
- 3.5 为什么用迭代器遍历元素时删除元素不会报错
- 3.6 遍历 Map 等集合时删除元素会抛出 ConcurrentModificationException吗
- 4. 如何正确地在遍历集合时删除元素
- 4.1 使用迭代器进行删除(推荐使用)
- 4.2 使用removeIf方法(推荐使用)
- 4.2.1 实现了Collection接口的集合
- 4.2.2 实现了Map接口的集合
- 4.3 收集要删除的元素,遍历结束之后再删除
- 5. 扩展:为什么用下标遍历元素集合时删除元素不会报错
0. 前言
本文讨论的是用增强 for 循环在遍历集合时删除元素的情况,用增强 for 循环在遍历集合时添加元素的情况类似
1. 问题产生的背景
在日常开发中,我们可能会遇到这样的需求,遍历一个集合,如果某些元素符合特定条件,我们就删除这些元素
我们以存储整数的 List 为例,如果是偶数,我们就从 List 中删除
import java.util.ArrayList;
import java.util.List;
public class ListDeleteTests {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
arrayList.add(i);
}
for (Integer i : arrayList) {
if (i % 2 == 0) {
arrayList.remove(i);
}
}
arrayList.forEach(System.out::println);
}
}
不出意外,运行代码后程序会抛出以下异常
Exception in thread “main” java.util.ConcurrentModificationException
at java.base/java.util.ArrayList
I
t
r
.
c
h
e
c
k
F
o
r
C
o
m
o
d
i
f
i
c
a
t
i
o
n
(
A
r
r
a
y
L
i
s
t
.
j
a
v
a
:
1013
)
a
t
j
a
v
a
.
b
a
s
e
/
j
a
v
a
.
u
t
i
l
.
A
r
r
a
y
L
i
s
t
Itr.checkForComodification(ArrayList.java:1013) at java.base/java.util.ArrayList
Itr.checkForComodification(ArrayList.java:1013)atjava.base/java.util.ArrayListItr.next(ArrayList.java:967)
at cn.edu.scau.ListTests.main(ListTests.java:15)
为什么会抛出这个异常呢,我们一步一步分析
2. Java中增强for循环的底层原理
增强 for 循环的底层原理基于迭代器(Iterator),以下是增强 for 循环的工作原理:
- 目标集合:增强for循环可以用于任何实现了
Iterable
接口的对象,这意味着它可以直接用于大多数集合类型,如List
、Set
和Queue
(增强for循环通过自动装箱和自动拆箱来处理基本数据类型) - 迭代器(Iterator):当增强for循环开始执行时,它会获取目标集合的迭代器。这个迭代器负责跟踪遍历的当前位置,并提供对集合中每个元素的访问
- 循环体:在每次迭代中,增强for循环调用迭代器的
next()
方法来获取下一个元素,并将其赋值给循环变量。然后执行循环体中的代码 - 结束条件:增强for循环会继续执行,直到迭代器的
hasNext()
方法返回false
,这表示没有更多元素可以遍历
增强 for 循环的一些关键点:
- 简洁性:增强 for 循环提供了更简洁的语法,不需要显式地创建迭代器
- 限制:增强 for 循环不支持在遍历时修改集合(添加、删除元素),因为这样做可能会导致
ConcurrentModificationException
- 只读访问:增强for循环通常用于只读操作,如果要进行修改操作,通常需要使用传统的
for
循环或迭代器
总的来说,增强 for 循环是 Java 提供的一个语法糖,内部使用迭代器来实现遍历操作,使得代码更加简洁和易读
3. 为什么增强for循环不支持在遍历集合时删除元素
3.1 问题排查
我们以文章最开始的例子为例
要想知道为什么增强 for 循环不支持在遍历集合时删除元素,我们需要一步步地排查,中间可能会涉及到阅读源码的过程
我们点击错误信息中给出的 ArrayList.java:1013
可以发现,当 modCount 变量和 expectedModCount 不相等时,就会抛出 ConcurrentModificationException
异常
哪里调用了 checkForComodification 方法呢,我们点击错误信息中给出的 ArrayList.java:967
可以发现,在用迭代器进行遍历时,每一次调用 next 方法都会调用一次 checkForComodification 方法,检查 modCount 和 expectedModCount 是否相等
那 modCount 和 expectedModCount 是从哪里来的呢
3.2 modCount 变量的来源
我们同时按下 CTRL 和 鼠标左键,查看 modCount 变量的来源,点击之后跳转到了 AbstractList 类的源码,而 ArrayList 继承了 AbstractList 类,说明 modCount 变量继承自 AbstractList 类
3.3 expectedModCount 变量的来源
那 expectedModCount 又是从哪里来的呢,按下 CTRL 和 鼠标左键,查看 modCount 变量的来源
可以发现,expectedModCount 变量来自于 ArrayList 的一个私有内部类 Itr,Itr 类实现了 Iterator 接口,而且 expectedModCount 变量的初值就是 modCount
3.4 导致modCount变量和expectedModCount不相等的原因
为什么 modCount 变量和 expectedModCount 不相等呢
我们查看 ArrayList 类 remove 方法的源码
remove 方法里面又调用了 fastRemove 方法
可以看到,fastRemove 方法在删除元素时会让 modCount 变量自增 1 个单位,导致 modCount 变量和 expectedModCount 变量不相等
3.5 为什么用迭代器遍历元素时删除元素不会报错
我们改成用迭代器遍历,在遍历过程中删除元素
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListDeleteTests {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
arrayList.add(i);
}
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
Integer i = iterator.next();
if (i % 2 == 0) {
iterator.remove();
}
}
arrayList.forEach(System.out::println);
}
}
运行程序,可以发现没有报错,为什么没有报错呢,增强 for 循环不也是基于迭代器的吗
为了弄清楚这个问题,我们需要查看一下ArrayList 中迭代器的源码
ArrayList 的迭代器是一个名为 Itr 的内部私有类,该内部私有类继承了顶层的 Iterator 接口
可以看到,每次获取迭代器返回的都是一个新的迭代器对象,新的迭代器对象的 expectedModCount 变量的值与 modCount 相等
我们继续查看迭代器提供的 remove 方法,按下 CTRL 和 鼠标左键,查看 remove 方法的源码
可以发现,remove 是 Iterator 接口中的默认方法,调用该方法会抛出异常
既然调用 ArrayList 迭代器的 remove 方法没有报错,说明 ArrayList 的迭代器必然重写了 remove 方法
我们查看 ArrayList 内部迭代器具体的 remove 方法,发现 ArrayList 内部的迭代器确实重写了 remove 方法,而且调用了 ArrayList 类的 remove 方法,而且调用完 ArrayList 类的 remove 方法之后,将 expectedModCount 变量重新赋值为 modCount,这也是为什么使用迭代器遍历元素时删除元素不会报错的原因
3.6 遍历 Map 等集合时删除元素会抛出 ConcurrentModificationException吗
上述的测试都是针对 List 集合来说的,那其它类型的集合呢
我们以 HashMap 为例,测试遍历 Map 时删除元素会不会抛出 ConcurrentModificationException 异常
import java.util.HashMap;
import java.util.Map;
public class MapDeleteTests {
public static void main(String[] args) {
Map<String, Object> hashMap = new HashMap<>();
for (int i = 0; i < 10; i++) {
hashMap.put("key" + i, i);
}
for (String key : hashMap.keySet()) {
if (Integer.parseInt(key.substring(3)) % 2 == 0) {
hashMap.remove(key);
}
}
hashMap.forEach((k, v) -> System.out.println(k + ":" + v));
}
}
不出意外,还是抛出 ConcurrentModificationException 异常,抛出异常的原因与 List 类似
HashMap 内部会维护 modCount 变量
而 expectedModCount 来自于 HashMap 内部的一个名为 HashIterator 的抽象类
HashIterator 类有三个具体的子类,分别对应 HashMap 的三种遍历方式
4. 如何正确地在遍历集合时删除元素
上面只是说到在用增强 for 循环遍历集合时删除元素会抛出异常的原因,那如果我真的要在遍历集合时删除元素(或者说添加元素),该怎么做呢
4.1 使用迭代器进行删除(推荐使用)
可以使用迭代器来遍历并删除元素,这样不会抛出异常
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListDeleteTests {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
arrayList.add(i);
}
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
Integer i = iterator.next();
if (i % 2 == 0) {
iterator.remove();
}
}
arrayList.forEach(System.out::println);
}
}
4.2 使用removeIf方法(推荐使用)
removeIf 方法的底层也是使用了迭代器,需要配合 lambda 表达式使用
removeIf 方法的源码如下(removeIf 方法来自 Collection 接口)
对于不同的集合来说,removeIf 方法的使用方式不同,总体可分为两类:
- 实现了 Collection 接口的集合
- 实现了 Map 接口的集合
4.2.1 实现了Collection接口的集合
以 List 集合为例
import java.util.ArrayList;
import java.util.List;
public class ListDeleteTests {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
arrayList.add(i);
}
arrayList.removeIf(i -> i % 2 == 0);
arrayList.forEach(System.out::println);
}
}
4.2.2 实现了Map接口的集合
以 HashMap 为例
import java.util.HashMap;
import java.util.Map;
public class MapDeleteTests {
public static void main(String[] args) {
Map<String, Object> hashMap = new HashMap<>();
for (int i = 0; i < 10; i++) {
hashMap.put("key" + i, i);
}
hashMap.keySet().removeIf(i -> Integer.parseInt(i.substring(3)) % 2 == 0);
hashMap.forEach((k, v) -> System.out.println(k + ":" + v));
}
}
4.3 收集要删除的元素,遍历结束之后再删除
import java.util.ArrayList;
import java.util.List;
public class ListDeleteTests {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
arrayList.add(i);
}
ArrayList<Integer> elementToDeleteList = new ArrayList<>();
for (int i = 0; i < arrayList.size(); i++) {
if (i % 2 == 0) {
elementToDeleteList.add(i);
}
}
elementToDeleteList.forEach(arrayList::remove);
arrayList.forEach(System.out::println);
}
}
5. 扩展:为什么用下标遍历元素集合时删除元素不会报错
对于可以用下标遍历的集合来说,可以在用下标遍历集合元素的同时删除元素,因为使用下标遍历集合并没有使用到迭代器
但是不建议在用下标遍历元素集合时删除元素,因为在我们的编程习惯中,用于遍历集合的下标都是只读的,如果我们修改了下标,可能会造成一些意想不到的问题
唯一需要注意的是,删除元素后下标要减一,否则会跳过某个元素
import java.util.ArrayList;
import java.util.List;
public class ListDeleteTests {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
arrayList.add(i);
}
for (int i = 0; i < arrayList.size(); i++) {
if (arrayList.get(i) % 2 == 0) {
arrayList.remove(i);
i--;
}
}
arrayList.forEach(System.out::println);
}
}