前言
小编之前剖析了 HashMap 和 ConcurrentHashMap 底层原理,需要的小伙伴可以点击传送门跳转。
步入正题
我们先来验证一下,foreach 中是不是真的不能对 arraylist 进行 remove/add 操作?
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String str : list) {
list.remove(str);
}
执行以上程序后会有以下结果:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.dmsd.test.virtual.TestVirtual.main(TestVirtual.java:32)
根据报错信息,我们可以得出以下信息:
- 报错位置在 main 方法中,异常类型为ConcurrentModificationException
- 内部报错位置在 ArrayList$Itr.checkForComodification ,是 ArrayList 的内部类 Itr 中的 checkForComodification 方法
得知以上信息了,我们看看程序代码经过反编译之后是什么样的?
List<String> list = new ArrayList();
list.add("3");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String str = (String)var2.next();
list.remove(str);
}
以上为程序反编译后的结果,我们可以看到抛开 foreach 语法糖外衣内部的实现方式,使用 ArrayList 内部实现的 Iterator 去遍历,每次遍历前会判断是否存在下一个元素,如存在获取下一个元素进行 remove
看起来没什么毛病,为什么报异常呢?我们来到 ArrayList 内部的 Iterator 实现过程去找找原因。
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
......
}
以上是 ArrayList 的内部类 Itr ,我们使用 foreach remove/add arraylist 元素时,使用的就是这个内部类。我们根据反编译后的代码来看看每个方法都是如何执行的?
var2.hasNext() 方法
// Itr 内部类的 hasNext()
public boolean hasNext() {
return cursor != size;
}
其中,size 是 ArrayList 类的成员变量,用来记录 arraylist 中的元素个数。cursor 是 ArryayList 内部类 Itr 的成员变量,是个遍历元素的指针。每次执行 hasNext() 都会判断当前指针是否指向了最后一个元素,如否,则代码有下个元素,返回 true
var2.next() 方法
// Itr 内部类的 next()
public E next() {
checkForComodification();
int i = cursor;
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];
}
我们再来看看 next() 是如何实现的?
我们看到 next() 内部第一行就调用了 checkForComodification() ,点进去看看是check 什么的。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
哦~ 这不就是我们刚才报的异常嘛! modCount 是 ArrayList 中的成员变量,用来记录list被修改结构的次数(add/remove 操作记为一次结构修改,而根据索引赋值不属于结构的修改);expectedModCount 是 ArrayList 内部类 Itr 的成员变量,上面我们可以看到在声明时候就用 modCount 给 expectedModCount 赋值了。而 next() 方法中获取 下一个元素值时,会首先判断 这两个字段是否相等,如不相等,则认为此list 的结构被修改过,这样拿到的下一个元素的值就不是准确的值,所以会直接抛出异常。
在foreach中当 remove/add 第一个元素后,其实是可以操作成功的,因为初始化内部类 Itr 时,会完成一次 expectedModCount = modCount; 赋值操作,所以此时 两个变量相等,不会抛出异常,会 操作成功。(经过打断点调试已验证)由于在 remove 过程中,会给 modCount++(add操作也一样会给 modCount++),记录此list结构的修改的次数增加了一次。所以,在 foreach 中当操作第二个元素时,并没有将 ArrayList 的成员变量 modCount 重新赋值给 ArrayList 内部类 Itr 的 expectedModCount 成员变量,导致两个变量值不等,以致于抛出 ConcurrentModificationException
// ArrayList 的 remove()
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}
明白了为什么不能在 foreach 中对 arrayList 进行 remove/add 操作了,那如果我们真的需要遍历对list进行操作怎么办?循环 remove/add 操作 arrayList 的正确姿势:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
iterator.remove();
}
我们分析一下,这两种写法本质上有什么区别?
调用 remove() 的对象不一样,一个是调用的 ArrayList 这个外部类的 remove 方法,一个调用的 ArrayList 内部类 Itr 的 remove() ,我们看看 ArrayList 内部类 Itr 的 remove() 是如何实现的?
// ArrayList 内部类 Itr 的 remove()
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 调用外部类 ArrayList 的 remove() 方法
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 将结构修改的次数重新复制给内部类变量expectedModCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
我们可以看到,在调用外部类 ArrayList 的 remove() 方法做了 remove 操作后,有一个给 expectedModCount 变量粉赋值的操作,也就是在 外部类的 remove() 中会给 modCount++ ,导致报异常的原因就是因为 expectedModCount 和 modCount 值不等,那索性每次 remove() 完后重新给 expectedModCount 赋值就能保证每次操作 list 后 两个变量值相等了,也就不会报异常了。