最近在项目中发现有人用到了在for循环中去进行列表的添加删除,最后报错,我就试着去研究底层代码。
发现问题
首先我们来看这段代码
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
//迭代器
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String next = iterator.next();
if(next.contains("1")){
list.remove(next);
}
}
}
我们发现这次会报这个错误
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.zsap.project.server.bootstrap.controller.PrjProjTeamController.main(PrjProjTeamController.java:87)
那我们如果用for循环呢
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
for (String s : list) {
if(s.contains("1")){
list.remove(s);
}
}
}
同样的报错
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.zsap.project.server.bootstrap.controller.PrjProjTeamController.main(PrjProjTeamController.java:84)
那如果是普通for循环呢
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
for (int i = 0; i < list.size(); i++) {
if(list.get(i).contains("1")){
list.remove(i);
}
}
}
运行正常
Process finished with exit code 0
那如果是迭代器删除呢
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
//迭代器
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String next = iterator.next();
if(next.contains("1")){
iterator.remove();
}
}
}
同样没有报错
Process finished with exit code 0
接下来我们从底层代码分析
底层分析
- 我们直接点进报错信息查看为什么报错(分析第一种情况迭代循环)
我们可以看到是modCount和count不一致所导致的异常发生。
接下来进行打断点发现是在**iterator.next()**所导致的报错
- 我们得确认这个是在那个具体的实现方法所报的错
使用对象.getClass().getName()进行查找
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
//迭代器
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String next = iterator.next();
System.out.println(iterator.getClass().getName());
if(next.contains("1")){
list.remove(next);
}
}
}
查看打印
java.util.ArrayList$Itr
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
我们这里发现是ArrayList类下的
逐步分析
public E next() {
//进行我们上一个分析出来报错的方法,也就是校验modcount和expectedModCount是否相等
checkForComodification();
//游标
int i = cursor;
//如果现在的游标已经大于数组大小报异常(ArrayList底层是动态数组,观察源码就能知道)
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
//游标向后移动
cursor = i + 1;
//返回游标移动前所在的元素
return (E) elementData[lastRet = i];
}
根据打断点我们发现是checkForComodification();方法报错
- 接下来我们要去找到为什么modcount和expectedModCount会不一致
我们查看源码发现modcount初始值为0
而expectedModCount初始化为modCount的值
我们查看list.remove()源码
public boolean remove(Object o) {
//如果传进来的对象是空的
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//遍历数组
for (int index = 0; index < size; index++)
//发现数组上的对应元素和传进来的对象相等
if (o.equals(elementData[index])) {
//删除操作
fastRemove(index);
return true;
}
}
return false;
}
可以发现关键代码是fastRemove(index);
继续往下
private void fastRemove(int index) {
//modCount++
modCount++;
//数组内排在被移除元素之后的元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
//这块代码完成了将被移除后部分的元素整体复制,所有下标往前挪一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将数组最后一位置为空
elementData[--size] = null; // clear to let GC do its work
}
在这段代码我们可以看到modCount是在这里发生了变化。
- 我们打断点之前发现modCount已经是4了,expectedModCount为3,说明在remove之前已经进行了modCount++,这时我们观察list的add()方法
public boolean add(E e) {
//我们看到jdk官方已经在这里注释了 增加modCount!!
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
继续往下
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
答案就在ensureExplicitCapacity方法
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
至此我们就能发现报错的原因,modCount相当于变化的次数,我们先往列表添加了三条,此时modCount为3,我们创建构造器,此时expectedModCount被modCount赋值初始化为3,再进行remove()方法,此时modCount为4,最后在next方法进行第一行校验时抛出异常。
- 那么为什么迭代器删除就不会报错呢?
其实观察迭代器的remove方法就能知道了
我们其实可以看到remove方法每次都保证了这两个值的一致性,也就不会报错啦!
总结
我们最后总结一下,其实就是迭代器并不知道你进行了删除操作,所以我期望的改变值不等于改变值,java认为此时是不安全的,就会快速失败。
而用迭代器自己的方法,就会保证迭代器知道我们每次改变的次数。
我们可以去研究普通for循环删除为什么不会报错发现在普通for循环中无论根据下标删除或者对象直接删除,都会去更新modCount记录改变次数,但并没有比较期望次数(这是迭代器独有的),所以不会去抛出异常,正常删除。
至于为什么增强for也会抛出异常,其实增强for是迭代器循环的语法糖,本质还是迭代器循环。
拓展
那有什么list可以在循环中删除保证安全呢,答案是copyOnWriteList,这个底层加了道锁,并且每次删除新增都是去复制一份数组进行的,篇幅问题,下一次细说。