源码角度分析Java 循环中删除数据为什么会报异常
一、源码角度分析Java 循环中删除数据为什么会报异常
相信大家在之前或多或少都知道 Java
中在增强 for
中删除数据会抛出:java.util.ConcurrentModificationException
异常,例如:如下所示程序:
public class RmTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("001");
list.add("002");
list.add("003");
list.add("004");
for (String l : list) {
if (Objects.equals(l, "002") || Objects.equals(l,"003")) {
list.remove(l);
}
}
System.out.println(list);
}
}
运行后会发现抛出了异常:
特别是一些新手小伙伴一不注意就陷入其中,当然解决方法也特别简单,可以转为迭代器,然后使用迭代器的 remove
方式删除数据,或者使用循环下标的方式通过下标进行删除,但需要注意正向循环和反向循环,如果是正向循环的话需要注意计算下标位置,不过不要担心,下面我们都会一一进行介绍。
首先来分析下为什么在增强 for
中会出现java.util.ConcurrentModificationException
异常,这里现将java
编译成class
形式,看增强 for
最终是以何种形式执行的:
javac RmTest.java
编译后的内容如下:
public class RmTest {
public RmTest() {
}
public static void main(String[] var0) {
ArrayList var1 = new ArrayList();
var1.add("001");
var1.add("002");
var1.add("003");
var1.add("004");
Iterator var2 = var1.iterator();
while(true) {
String var3;
do {
if (!var2.hasNext()) {
System.out.println(var1);
return;
}
var3 = (String)var2.next();
} while(!Objects.equals(var3, "002") && !Objects.equals(var3, "003"));
var1.remove(var3);
}
}
}
可以看到增强for
最终是编译成迭代器的方式进行遍历数据,但需要注意的是删除数据依然使用的 List
中的 remove
方法,通过抛出的异常链可以看出,问题发生在了 next
方法中的 checkForComodification
方法下:
下面看到 ArrayList
下迭代器的 next
方法中,在 Itr
类下:
在这个方法中首先调用了 checkForComodification
方法,正好上面的异常链中也涉及到了 checkForComodification
方法,下面进到该方法中:
这里是不是看到了熟悉的 ConcurrentModificationException
异常,只要 modCount
和 expectedModCount
不相等就会抛出该异常,下面看下 expectedModCount
的声明位置:
在迭代器内部声明的,并且起始值等于 modCount
,而 modCount
则在定义在 AbstractList
在迭代器的外部,这里还记得前面迭代器中使用的是 List
中的 remove
方法删除的数据,这里看到该方法中:
该方法实际的删除逻辑在 fastRemove
方法中,继续看到该方法下:
看到这里是不是很直观了,modCount
数值发生了变化,而迭代器中的expectedModCount
没有随之修改,就导致 expectedModCount != modCount
而抛出异常。
我们都知道使用迭代器中的 remove
方式是不会引发异常的,比如:
public class RmTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("001");
list.add("002");
list.add("003");
list.add("004");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String l = iterator.next();
if (Objects.equals(l, "002") || Objects.equals(l, "003")) {
iterator.remove();
}
}
System.out.println(list);
}
}
运行结果:
为什么迭代器的 remove
可以呢,下面看到该方法中:
可以看出迭代器的 remove
同样也是使用了 List
中的 remove
方法,但它会在删除后重置 expectedModCount
的值,使其保持和 modCount
一致,因此就不会触发上面的异常。
看到这里应该明白为什么会抛出异常了,但为什么这样设计呢?这里可以总结下其中,modCount
主要表示集合被修改的次数,expectedModCount
表示迭代器内部维护的集合被修改的次数。当modCount
和expectedModCount
不相等时,则表示肯定有其他某个地方对集合进行了修改,此时,如果继续使用迭代器遍历集合,就可能会出现遍历到非预期的元素或者下个元素不存在了,因此只要expectedModCount
和modCount
保持一致,数据就可认为是可信的。
通过这里也能给我们警醒,如果需要在并发情况下操作集合一定要选用线程安全的集合。
下面再补充下如果不用增强for
,使用下标自增的方式删除是否可行吗?
public class RmTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("001");
list.add("002");
list.add("003");
list.add("004");
for (int i = 0; i < list.size(); i++) {
String l = list.get(i);
if (Objects.equals(l, "002") || Objects.equals(l,"003")) {
list.remove(i);
}
}
System.out.println(list);
}
}
运行后:
发现 003
并没有被移除,因为当移除了 002
后,002
后的数据顺势向前移位,原本003
的下标为 2
,移位后变成了 1
,但下标 i
继续增长,便会错过后面的数据,那怎么解决呢,既然后面的数据向前移位,对下标i
也向前移位就是了:
public class RmTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("001");
list.add("002");
list.add("003");
list.add("004");
for (int i = 0; i < list.size(); i++) {
String l = list.get(i);
if (Objects.equals(l, "002") || Objects.equals(l,"003")) {
list.remove(i);
i = i-1;
}
}
System.out.println(list);
}
}
运行后数据正常:
既然正向遍历下标需要移位,那如果反过来反向循环不就可以不管下标了吗:
public class RmTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("001");
list.add("002");
list.add("003");
list.add("004");
for (int i = list.size() - 1; i >= 0; i--) {
String l = list.get(i);
if (Objects.equals(l, "002") || Objects.equals(l, "003")) {
list.remove(i);
}
}
System.out.println(list);
}
}
运行后数据正常: