在我们迭代操作集合时,有时可能需要改变集合的元素,若操作不当,经常都会出现不正确的结果或者并发操作异常,如下:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(Unknown Source)
at java.util.AbstractList$Itr.next(Unknown Source)
at com.qiuyang.Test.Test.main(Test.java:14)
在学习JAVA的时候,大家都知道在迭代集合时,不能去修改集合,但是WHY? 在查看API集合部分源码时,终于找到了原因,先来个例子:
import java.util.*;
public class Test {
public static void main(String[] args){
ArrayList<String> list=new ArrayList<String>();
list.add("张三");
list.add("李四");
list.add("王五");
list.add("赵六");
for( Iterator<String> it=list.iterator();it.hasNext();){
String name=it.next();
if("李四".equals(name)){
list.remove(name);//集合操作,改变了集合的版本,但不会改变迭代器的版本
}else
System.out.print(name+" ");
}
System.out.println("\n"+list);
}
}
执行结果:
张三 Exception in thread "main" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(Unknown Source)
at java.util.AbstractList$Itr.next(Unknown Source)
at com.qiuyang.Test.Test.main(Test.java:14)
可以看到,程序打印了张三,然后出现并发修改异常,那这个异常时怎么产生的呢?我们找到AbstractList的源代码,取出其中部分,如下:
在AbstractList类中
内部成员有:
protected transient int modCount = 0;
注:该modCount相当于集合的版本号,集合内容每改变一次,其值就+1。
观察下面内部类可以知道,在next()、remove()方法都会去判断集合版本号与迭代器版本号是否一致checkForComodification();
内部方法有:
public Iterator<E> iterator() {
return new Itr();
}
还有一个内部类:
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 {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;//迭代器进行删除时,会改变it的版本
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
//检查迭代器的版本与集合的版本是否一致,不同则抛出异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
可以看出,迭代器的next()方法,每次都会检查版本号是否一致,modCount为集合的版本号,excepectedModCount为迭代器的版本号,当版本号不一致时,则出现并发修改异常。下面具体分析一下执行过程:
该案列中,集合list.size()=4,遍历集合时创建集合迭代器,此时有 expectedModCount = modCount=4,迭代器的cursor=0,
(1)cursor=0,it.next()检查版本号一致,取出“张三”,cursor=cursor+1=1,打印,然后it.hasNext()函数判断cursor!=list.size(),返回true
(2)cursor=1,it.next()检查版本号一致,取出“李四”,cursor=cursor+1=2,进入if语句,移除了“李四”,此时集合内容改变,版本号modCount++,为5.然后it.hasNext()判断cursor(2)!=size(4),true
(3)cursor=2,it.next()检查发现版本号不一致expectedModCount! = modCount,抛出并发修改异常。
若将list.remove(name)改为迭代器的操作it.remove(),则会得到正确的结果如下,这是因为迭代器的remove()方法修改了版本号,如上源代码所示。
张三 王五 赵六
[张三, 王五, 赵六]
二、下面看一个更有意思的例子,同样为集合的并发修改,却没有抛出异常
import java.util.*;
public class Test {
public static void main(String[] args){
ArrayList<String> list=new ArrayList<String>();
list.add("张三");
list.add("李四");
list.add("王五");
list.add("赵六");
for( Iterator<String> it=list.iterator();it.hasNext();){
String name=it.next();
if("王五".equals(name)){
list.remove(name);//集合操作,不会改变迭代器的版本,会抛出异常
}else
System.out.print(name+" ");
}
System.out.println("\n"+list);
}
}
执行结果为:
张三 李四
[张三, 李四, 赵六]
可以看到,程序虽然没有抛出异常,但却并没有遍历完整个集合,意味着迭代器提前停止了,具体分析如下:
(1)cursor=0; it.next()检查版本号一致,取出“张三”,cursor++为1,打印“张三”,然后, it.hasNext()判断cursor(1)!=size(4),true;
(2)cursor=1,it.next()检查版本号一致,取出“李四”,cursor++等于2,打印李四,然后,it.hasNext()判断cursor(2)!=size(4);
(3)cursor=2,it.next()检查版本号一致,取出“王五”,cursor++等于3;进入if语句,list.remove(“王五”),此时,版本号modCount++,且集合大小改变list.size()=3,然后,it.hasNext()判断发现cursor(3)==size(3),返回false,迭代结束,故得到结果:张三李四
(也是因为it.hasNext()返回false退出循环,所以才没有再进入it.next()函数,故没有产生并发修改异常)
若将list.remove(name)语句改为 it.remove(),即用迭代器的操作去删除集合的元素,则会得到正确的结果,如下:
张三 李四 赵六
[张三, 李四, 赵六]
总结:在对集合进行迭代时,不要去操作集合(指的是用集合去增删元素),但可以用迭代器去操作集合,it.remove(),对于List集合,还可以用ListIterator进行add()操作;
造成上述现象原因,集合操作会改变集合版本号modCount而不会改变迭代器的版本号excepectedModCount,而迭代器操作则集合版本号和迭代器的版本号都会同步的改变,故不会产生异常。