跳至主要內容

foreach实现原理,避免ConcurrentModificationException

cylin...大约 3 分钟

总览

并发修改异常是指:ConcurrentModificationException

foreach循环是JDK1.5开始引入的,这种方式遍历集合或数组,代码更加简洁

如果是数组,会转成数组的遍历方式:

for(int i=0;i<array.length;i++) {
    Type item = array[i];
    /* body-of-loop */
}

如果是遍历集合类型,则要求被遍历的集合类型实现java.lang.Iterable接口,在iterator()方法中返回一个Iterator迭代器。

//Iterable.java
public interface Iterable<T> {
    Iterator<T> iterator();
}
//Iterator.java
public interface Iterator<E> {
    boolean hasNext();
    E next();
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
}

相应的foreach代码会被编译器转换成Iterator的迭代方式:

for(Iterator<Type> iter = list.iterator(); iter.hasNext(); ) {
    Type item = iter.next();
    /* body-of-loop */
}

使用限制

  1. foreach在遍历过程中不能修改集合中元素的值。不过,如果遍历的是数组,则不受此限制
  2. foreach在遍历过程中不能往集合中增加或删除元素,否则ConcurrentModificationException异常。即使在个别特殊情况下没有抛出这个异常,那也是因为巧合(下文会有说明)
  3. 遍历过程中,集合或数组中同时只有一个元素可见,即只有“当前遍历到的元素”可见,而前一个或后一个元素是不可见的
  4. 只能从前往后正向遍历,不能反向遍历
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
final String toRemove = "2";
final String toAdd = "1000";
for (String item : list) {
    //item = "100"; //这句执行无效,仅仅改变item的指向,不会改变list中的元素
    if (toRemove.equals(item)) {
        list.remove(item); //仅当toRemove为"3"时,没有报异常。这是删除倒数第二个元素情况下的“巧合”。
        //list.add(toAdd); // 报ConcurrentModificationException
    }
}

原因分析

  1. ArrayList内部有一个成员变量modCount,记录list内部元素改变的次数
  2. 通过iter=list.iterator()返回一个新的迭代器对象的时候,iter内部会用expectedModCount成员变量记录下当时的modCount的值。
  3. 在整个循环的遍历过程中,不管是iter.next()还是iter.remove()方法都会检查原ArrayListmodCount值是否与iter内部记录的expectedModCount值一致,一旦不一致就会抛ConcurrentModificationException
  4. 因此,异常是在增减集合元素后,下一轮循环的iter.next()方法中抛出的
public boolean hasNext() {
    return cursor != size;
}
public E next() {
	// 此处抛出异常
    checkForComodification();
    // 省略其他代码...
}
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

为什么会有巧合?

因为,这种情况下,一旦remove之后,原ArrayListsize会减少1,下一轮通过iter.hasNext()hasNext只是返回迭代器内部的迭代位置cursor是否已达到被迭代容器的size,本身不会抛异常)判断是否还有元素时,发现没有了,直接返回false,进而不会调用到iter.next()方法。当然也就不会有从这个方法中抛出的异常啦

正确使用

如果要在遍历集合的过程中需要删除或添加元素该怎么办

普通for循环

for(int i=0;i<list.size();i++) {
    String item = list.get(i);
    if ("3".equals(item)) {
        list.remove(i);//为了效率,这里最好不要用list.remove(item)
    }
}

迭代器

Iterator<String> iter = list.iterator();
while(iter.hasNext()) {
    String item = iter.next();
    if ("4".equals(item)) {
        iter.remove();
    }
}