使用迭代器对java中List遍历时,程序抛出了ConcurrentModificationException异常。这是由于Java的 fast-fail 机制(快速失败)导致的,可以提前预料遍历失败情况。看下面的例子。
public static void main(String[] args) {
ArrayList list = new ArrayList<String>(){{
this.add("1");
this.add("2");
this.add("3");
this.add("4");
}};
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object o = iterator.next();
if("2".equals(o)){
list.remove(o);//异常关键
}
}
}
异常发生的关键是在使用迭代器遍历过程中,调用list的remove或者add方法,对所遍历的对象进行了修改。
首先看一个简单for循环例子,如果没有java的fast-fail机制,到底会出现什么问题。我们遍历list集合,移除其中的"2"元素。
public static void main(String[] args) {
ArrayList list = new ArrayList<String>(){{
this.add("1");
this.add("2");
this.add("2");
this.add("1");
}};
for (int i = 0; i < list.size(); i++) {
Object o = list.get(i);
System.out.println("遍历到"+o);
if("2".equals(o)){
list.remove(o);
}
}
System.out.println(list);
}
程序输出:
遍历到1
遍历到2
遍历到1
[1, 2, 1]
可见,list中没有移除所有的"2"元素。这个残余的"2"其实是第二个"2"。为什么会出现这种问题,原因很简单。在遍历到第一个"2"时,移除了这个元素,为填补这个空缺,后面的元素要向前移动。这时,第二个”2“元素移动到了第一个”2“的位置。在下一躺循环,访问的是元素‘1’,跨过了第二个”2“。
实际上,我们对一个集合遍历时,如果这个集合删除或者增加 了元素,都会对遍历造成影响。
所以在循环一个集合时,尽量不要增加或者删除这个集合中的元素。
我们再回到刚才的异常。
public static void main(String[] args) {
ArrayList list = new ArrayList<String>(){{
this.add("1");
this.add("2");
this.add("3");
this.add("4");
}};
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object o = iterator.next();
if("2".equals(o)){
list.remove(o);//异常关键
}
}
}
为了避免产生上面例子中的错误,使用迭代器iterator对List进行遍历的时候,java是不允许我们直接调用List.remove或者List.add方法对集合进行修改的,否则会抛出ConcurrentModificationException异常。
但是Java是如何实现这种检测机制的呢,看下面源码。
在ArrayList的父类AbstractList中,成员变量modCount 记录对集合的修改次数。调用ArrayList中add或者remove方法时,都会使modCount +1;
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
...
protected transient int modCount = 0;//记录对集合的修改次数
...
}
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
public boolean add(E e) {
...
modCount++;
...
}
public E remove(int index) {
...
modCount++;
...
}
}
每一次获取ArrayList的迭代器时,会在迭代器对象中用expectedModCount保存此时的ArrayList修改次数。使用Iterator.next方法获取下一个元素时,首先检查modCount、 expectedModCount是否还相等,如果不相等(ArrayList已经被修改),抛出ConcurrentModificationException异常。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
...
int expectedModCount = modCount; //保存当前ArrayList修改次数。
...
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
...
}
...
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}
使用迭代器、foreach循环遍历时,尽量不要直接调用ArrayList中的add或者remove。java在Iterator迭代器中提供了remove方法,移除ArrayList中的元素。像下面这样。
public static void main(String[] args) {
ArrayList list = new ArrayList<String>(){{
this.add("1");
this.add("2");
this.add("2");
this.add("1");
}};
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object o = iterator.next();
if("2".equals(o))
{
iterator.remove();
}
System.out.println(o);
}
System.out.println(list);
}
测试结果:
1
2
2
1
[1, 1]
为什么调用迭代器中的remove方法就不会抛出异常的呢,我们看下源码。
private class Itr implements Iterator<E> {
int cursor; // 下一次要返回的元素索引
int lastRet = -1; // 最后一次返回的元素索引
int expectedModCount = modCount;
...
/**
调用next方法,主要是返回当前cursor所指向的元素,
然后让lastRet +1指向这个元素.
**/
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];
}
/*
为了避免遍历过程中移除元素造成漏掉一些元素。
在移除元素后要对cursor、lastRet 做后移操作。下一次循环还访问当前位置的元素
(当前位置元素已经被移除,新元素占当前位置)
并且要更新迭代器中记录的ArrayList修改次数。
*/
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet; //cursor 后退
lastRet = -1;//lastRet 后退1
expectedModCount = modCount; //更新迭代器中记录的ArrayList修改次数。
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
迭代器中有两个变量 cursor,记录下一次要返回的元素索引,lastRet 记录最后一次返回的元素索引。调用next()方法时,返回cursor指向的元素,然后cursor和lastRet都加一。如果在循环中调用了Iterator.remove方法,会让cursor、lastRet都都退一位,避免遍历漏掉元素。
ArrayList不是线程安全的。单线程中,使用迭代器遍历时,我们避免了直接调用ArryList的add、remove方法。也应考虑到多线程时,某个线程迭代器遍历ArryList时,避免其他线程直接对ArrayList进行修改,否则一样会抛出异常。