迭代器是依赖于集合而存在的,在判断成功后,集合的中新添加了元素,而迭代器却不知道,所以就报错了,这个错叫并发修改异常。
一:问题代码:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class Test {
public static void main(String[] args) {
Collection<String> c=new ArrayList<String>();
c.add("a");
c.add("b");
c.add("c");
Iterator<String> itr=c.iterator();
while(itr.hasNext()){ //迭代器遍历,但是下面却用了集合的remove方法
String str= (String)itr.next();
if(str.equals("a"))
c.remove("b");
}
}
}
报错信息如下:
二:问题分析
问题出现场景:ConcurrentModificationException:当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。
换句话说就是:迭代器遍历的时候只能用迭代器的remove()函数,如果用集合对象的remove函数就会爆出并发修改异常。
我们选择一步一步查看源代码:我们想着直接查 checkForComodification()函数,但是不行。所以我们先找到ArrayList,然而ArrayList类中根本没有迭代器的实现,所以我猜测Iterator大概是在他的父类或者接口中。
不出所料:在AbstractList类内果然发现了迭代器的实现函数,代码很简短,从这段代码可以看出返回的是一个指向Itr类型对象的引用,但是又出现了我们不认识的Itr(),所以接着去找
//下面是AbstractList的代码,只保留了内部类Itr和部分有用的代码。下面会拆开详细讲
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
public Iterator<E> iterator() {
return new Itr();
}
//可以看出Itr就是AbstractList的一个成员内部类
private class Itr implements Iterator<E> {
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(e);
}
}
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();
}
}
这里先讲两个变量:
expectedModCount:这个变量是AbstractList里面的内部类ltr中的变量。代表预期的修改次数,通过public Iterator<E> iterator() { return new Itr(); }这个函数的时候会自动new一个Itr对象,同时会用modCount为其初始化。
int expectedModCount = modCount;
modCount:这个是AbstractList类中的变量。表示实际的修改次数。
protected transient int modCount = 0;
该值表示对List的修改次数,查看ArrayList的add()和remove()方法就可以发现,每次调用add()方法或者remove()方法就会对modCount进行加1操作。
好了,到这里我们再回到最初的程序:
当调用c.iterator()返回一个Iterator之后,通过Iterator的hashNext()方法判断是否还有元素未被访问,我们看一下hasNext()方法,hashNext()方法的实现很简单:
public boolean hasNext() {
return cursor != size();
}
如果下一个访问的元素下标不等于ArrayList的大小,就表示有元素需要访问,这个很容易理解,如果下一个访问元素的下标等于ArrayList的大小,则肯定到达末尾了。
然后通过Iterator的next()方法获取到下标为0的元素,我们看一下next()方法的具体实现:
public E next() {
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
这里是非常关键的地方:首先在next()方法中会调用checkForComodification()方法,然后根据cursor的值获取到元素,接着将cursor的值赋给lastRet,并对cursor的值进行加1操作。
初始时,cursor为0,lastRet为-1,那么调用一次之后,cursor的值为1,lastRet的值为0。
注意此时,modCount为0,expectedModCount也为0。
接着往下看上面的出错程序,程序中判断当前元素的值是否为a,若为a,则调用对象中的remove()方法(而不是迭代器中的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; // Let gc do its work
}
通过remove方法删除元素最终是调用的fastRemove()方法,在fastRemove()方法中,首先对modCount进行加1操作(因为对集合修改了一次),然后接下来就是删除元素的操作,最后将size进行减1操作,并将引用置为null以方便垃圾收集器进行回收工作。
那么注意此时各个变量的值:对于对象c的迭代器itr,其expectedModCount为0,cursor的值为1,lastRet的值为0。
对于对象c本身,其modCount为1,size为0。
接着看程序代码,执行完相应操作后,经过判断继续while循环,调用hasNext方法()判断,然后继续调用迭代器itr的next()方法:
注意,此时要注意next()方法中的第一句:checkForComodification()。
在checkForComodification方法中进行的操作是:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
很显然,此时modCount为1,而expectedModCount为0,modCount不等于expectedModCount,因此程序就抛出了ConcurrentModificationException异常。
错误是在第一次while循环remove函数调用完埋下的,是第二次while循环next()函数时爆出来的。
关键点就在于:调用list.remove()方法导致modCount和expectedModCount的值不一致。也就是AbstractList的内部类中的Itr内部类只初始化一次,如果初始化之后再跳出内部类进行其他操作的话,modCount的值就可能改变,但是exceptedModCount的值却无法同步改变了。这就出错了,而且并发修改异常也就很容易理解为什么要这样叫了。
注意,像使用for-each进行迭代实际上也会出现这种问题。
三:问题解决。
1:单线程下
上面以及说过了:迭代器遍历的时候用集合的函数修改就会抛出并发修改异常。
修改方法1.迭代器遍历,调用迭代器函数进行操作。
Iterator<String> itr=c.iterator();
//1.迭代器遍历,调用迭代器的操作函数
while(itr.hasNext()){
String str= (String)itr.next();
if(str.equals("a"))
itr.remove();
}
修改方法2.传统的对象for循环遍历,调用对象函数进行操作。
//2.选用普通的for循环遍历,调用对象的操作函数
for(int i=0;i<c.size();i++){
//这里必须进行一个强制的类型转换,取出来的是Object类型的对象,你得转换成String
String str= ((ArrayList<String>) c).get(i);
if(str.equals("a"))
c.remove("a");
//一个小细节,iterator里的remove函数不需要传参,对象的需要传参
}
总之就是两条线路各走各的,不能交叉。
2:多线程下
多线程下的见下面的参考网址:https://www.cnblogs.com/bsjl/p/7676209.html