我们都知道集合循环删除元素,要使用iterator和while循环,不能用for和foreach。

foreach会抛出异常java.util.ConcurrentModificationException,具体原因是什么呢?

先来看一段代码,摘自阿里巴巴的java开发手册

List<String> list = new ArrayList<String>();
 list.add("1");
 list.add("2");
 for (String temp : list) {
    if("1".equals(temp)){
         list.remove(temp);
	} 
}

此时执行代码,没有问题,但是需要注意,循环此时只执行了一次。具体过程后面去分析。再来看一段会出问题的代码:

List<String> list = new ArrayList<String>();
 list .add("1");
 list .add("2");
 for (String temp : list ) {
    if("2".equals(temp)){
         list .remove(temp);
	} 
}

输出为:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at MyTest.test(MyTest.java:32)
	at MyTest.main(MyTest.java:25)

是不是很奇怪?接下来将class文件,反编译下,结果如下

1 List list = new ArrayList();
 2 list.add("1");
 3 list.add("2");
 4 Iterator i$ = list.iterator();
 5 do
 6 {
 7     if(!i$.hasNext())
 8         break;
 9     String temp = (String)i$.next();
10     if("1".equals(temp))
11         list.remove(temp);
12 } while(true);

几个需要注意的点:

1.foreach遍历集合,实际上内部使用的是iterator。
2.代码先判断是否hasNext,然后再去调用next,这两个函数是引起问题的关键。
3.这里的remove还是list的remove方法。

先去观察下list.remove()方法中的核心方法fastRemove()方法。

1 private void fastRemove(int index) {
2         modCount++;
3         int numMoved = size - index - 1;
4         if (numMoved > 0)
5             System.arraycopy(elementData, index+1, elementData, index,
6                              numMoved);
7         elementData[--size] = null; // clear to let GC do its work
8     }

注意第二行,modCount++,此处先不表,下文再说这个参数。

顺路观察下list.add()方法

1 public boolean add(E e) {
2         ensureCapacityInternal(size + 1);  // Increments modCount!!
3         elementData[size++] = e;
4         return true;
5     }

注意第二行的注释,说明这个方法也会使modCount++

再去观察下,iterator()方法

public Iterator<E> iterator() {
         return new Itr();
}
private class Itr implements Iterator<E> {
    //游标记录当前索引的位置,是从0开始的
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;//记录元素修改记录,若在迭代List时,modCount发生变化将会抛出ConcurrentModificationException异常

    //判断游标是否到达最后的位置,若没有表示还有元素,若有则没有元素了
    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        //校验是否被修改(其实这里存在多线程问题,所以说ArrayList不是线程安全的)
        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;//游标向后移动1
        return (E) elementData[lastRet = i];//返回值并且赋值
    }

    public void remove() {
        //lastRet初始值为-1,若需要调用remove方法,则bi
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            //删除lastRet索引处的元素
            ArrayList.this.remove(lastRet);
            
            //游标前移,由于是删除remove方法删除的是lastRet位置的元素则游标需要前移才能保证可以遍历到所有的元素
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    //检查列表是否修改,若修改则抛出异常
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

几个需要注意的点:
1.在iterator初始化的时候(也就是for循环开始处),expectedModCount = modCount,猜测是和当时list内部的元素数量有关系(已证实)。
2.当cursor != size的时候,hasNext返回true
3.next()函数的第一行,checkForComodification()这个函数就是报错的原因 这个函数就是万恶之源
4.checkForComodification方法中,mod != expectedModCount 就会抛出ConcurrentModificationException()

接下来分析文章开头的第一个例子,为啥不会报错?

第一个例子
初始化:expectedModCount = modCount = 2;cursor = 0; size = 2
第一次循环:list.remove()调用fastRemove(), modCount++,size–;
执行完第一次循环后:expectedModCount =2,modCount =3,cursor = 1,size = 1 执行hasNext()返回false,程序不会报错,但只执行了一次循环。

第二个例子
初始化:expectedModCount = modCount = 2;cursor = 0; size = 2
第一次循环:无操作
执行完第一次循环后:expectedModCount =2,modCount =2,cursor = 1,size = 2 执行hasNext()返回true,进入下次循环。
第二次循环:list.remove()调用fastRemove(), modCount++,size–;
执行完第二次循环后:expectedModCount =2,modCount =3,cursor = 2, size = 1 此时cursor != size,hasNext()返回true,继续执行循环,调用next方法,mod != expectedModCount ,抛出异常ConcurrentModificationException

至此,关于为什么foreach循环中remove抛异常ConcurrentModificationException,原因已经很明确了。
那为什么使用iterator和while,调用iterator.remove就没问题呢,其实源码里已经很明确了,iterator自带remove每次执行完毕都会重置expectedModCount。
手册上推荐的代码如下

1 Iterator<String> it = a.iterator(); while(it.hasNext()){
2 String temp = it.next(); if(删除元素的条件){
3         it.remove();
4        }
5 }

此时remove是iterator的remove,我们看一下它的源码:

public void remove() {
        //lastRet初始值为-1,若需要调用remove方法,则bi
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            //删除lastRet索引处的元素
            ArrayList.this.remove(lastRet);
            
            //游标前移,由于是删除remove方法删除的是lastRet位置的元素则游标需要前移才能保证可以遍历到所有的元素
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

注意第11-14行,每次删除后cursor前移一位,expectedModCount = modCount,所以此时程序不会有之前的问题。

但是手册上推荐的方法,在多线程环境还是有可能出现问题,一个线程执行上面的代码,一个线程遍历迭代器中的元素,同样会抛出CocurrentModificationException。
如果要并发操作,需要对iterator对象加锁。

拓展:
1.modCount是指ArrayList的修改次数,每次add或remove都会自增,
当迭代时,就是将这个modCount暂存在expectedModCount中,
每次获取下一个元素时,都检查下修改次数是否有变动,有变动则不再继续迭代,而是抛出错误ConcurrentModificationException
这样就强制要求在迭代时不能进行remove/add操作,而foreach会编译成迭代,所以foreach时也不能进行remove/add操作

2 再看一个例子

public static void main(String[] args) throws Exception {
        List<String> a = new ArrayList<String>();
        a.add("1");
        a.add("2");
        for (String temp : a) {
            System.out.println(temp);
            if("2".equals(temp)){
                a.add("3");
                a.remove("2");
            } 
        }
}

此时输出为:
1
2

显然,程序并没有执行第三次循环,第二次循环结束,cursor再一次等于size,程序退出循环。
与remove类似,将文章开头的代码中remove替换为add,我们会发现无论是第一个例子还是第二个例子,都会抛出ConcurrentModificationException错误,原因同上。

3.for循环正向遍历不抛异常,但每次remove元素,后面的元素下标 -1,每次循环 i++,会导致遍历不完整,进而导致删除不完全,解决思路:
1.list.remove之后不执行 i++

List<Integer> list = new ArrayList<Integer>();
 list.add(2);
 list.add(1);
 list.add(1);
 list.add(3);
 for (int i = 0; i < list.size(); i++) {
     Integer integer =  list.get(i);
      if (integer == 1){
          list.remove(i);
          i--;
      }
  }

2.for反向遍历。

for (int i = list.size()-1; i >= 0; i--) {
     Integer integer =  list.get(i);
     if (integer == 1){
         list.remove(i);
     }
 }

以上两种都是可以的,但是太low,且每次remove之后,i后面的元素都要换下标,效率不好,不推荐使用。