问题起因
闲来无事看起了java开发手册,发现一个有意思的地方,我整理了一下,大概如下:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
if ("2".equals(item)) {
list.remove(item);
}
}
for (String item:list){
System.out.println(item);
}
}
手册说禁止使用上面的方法,我就很奇怪,没毛病呀,为啥呢,一运行报错了。我先尝试了“1”情况,输出2很正常,但是变为2的时候,输出如下:
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 Main.main(Main.java:10)
为什么会这样呢,我就去研究了一下。
分析过程
先查看哪里出错了呢!找到了位置如下:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
显然是modCount与exceptedModCount不同造成的,然后对两个参数分析。
分析modCount,发现这个变量是继承父类AbstractList而来,默认值为0。
protected transient int modCount = 0;
可是这个变量什么时候尽心改变的呢?这个时候需要追本溯源了,为啥会比较两个值,是因为ArrayList即从List中继承了iterator(),又从AbstractList中继承了list。
为什么呢,是为了实现ArrayList的Fail-Fast机制。Fail-Fast机制的作用是避免ArrayList在迭代过程中数组结构发生变化的问题。
所以会进行两个数据的判断,查看是否改变了数据结构。查看一下AbstractList中的iterator()方法。
idk1.8如下:
public Iterator<E> iterator() { return listIterator(); }
只是返回了一个Iterator的数据结构,在进一步查看listIterator()方法
public ListIterator<E> listIterator() {
return listIterator(0);
}
调用了listIterator(0)这个方法,注意传入了新的参数,不是之前的那个方法了,进一步剖析!!发现下面一个方法只是先判断是否数组下标越界,然后返回了一个新的数据结构。
public ListIterator<E> listIterator(final int index) {
rangeCheckForAdd(index);
return new ListItr(index);
}
private class Itr implements Iterator<E> {
int cursor = 0;
int lastRet = -1;
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
这里省略了一步,他继承了Itr,直接分析Itr容易理解。重要是对上面的四个值进行变化分析:
代码拆分理解:
int cursor = 0;
int lastRet = -1;
int expectedModCount = modCount;
cursor:表示下一个要访问的元素的索引,从next()方法的具体实现就可看出
lastRet:表示上一个访问的元素的索引
expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。
modCount是AbstractList类中的变量,前面分析到过,该值表示对List的修改次数
接着是hasNext()方法,判断访问是否还有未访问的值!!!!
public boolean hasNext() { return cursor != size(); }
紧接着执行next,可能有人问,你是foreach呀,为啥会next呢,对ArrayList进行遍历的时候,其实就是执行的next,因此分析next!!!
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
checkForComodification()方法就是之前说,判断数据结构是否发生改变的,也就是进行比较查看这中间当前的list中的数据是否发生了改变。每次增加修改等操作都会对modCount进行+1操作,exceptedModCount初始值就是modCount。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
继续分析next方法:现将下一个要访问的元素用i进行存储,然后获取下一个元素next,最后一次操作的元素变为i,下一个元素跟新为i+1,然后返回要操作的元素next。此时最值得注意点就是,modCount与exceptedModCount的值相等,都为0。
然后执行remove方法,调用了ArrayList中的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;
}
private void fastRemove(int index) {
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
}
较为关键在每次remove方法都会遍历当前数据,判断是否存在,如果存在调用fastRemove方法,而这里面 modCount++是第一步操作 ,再操作之前modCount就为1了,而此时执行判断时,两者不想等,自然会报错。我们在分析一下为什么第一个不会报异常,看源码可以看出,倒数第一个元素的时候,Itr的cursor是1,当list删除掉一个元素的时候,size也是等于1,所以在判断的时候,cursor==size,因此就会在删除了一个元素以后,直接跳出循环,所以不会报异常了。
总结
foreach遍历内部也就是与iterator一致,实例化为iterator对象。从之前的源码可以看出,在remove的过程中,modCount会改变,因此在一个循环中我们不要同时采用两种方法。如下:
ArrayList<String> list = new ArrayList<>();
list.add("2");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String s= iterator.next();
if(s.equsls("2"))
list.remove(s);
}
这样是与之前的foreach循环属于同样的情况,值得注意。另外,
不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
上面的正确写法如下:
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("2")) {
iterator.remove();
}
}
参考:阿里巴巴Java开发手册终极版v1.3.0.pdf
积土成山,风雨兴焉!!!