关于ConcurrentModificationException我有话要说

646 阅读2分钟

关于这个比较基础的问题本来是不想记录的。但是为啥还是要记录呢,原因我就不多说了。

关于ConcurrentModificationException,很多人第一次见到这个异常都是因为对List进行foreach操作,然后删除元素。

foreach的实现原理其实就是iterator,可能很多人都会想使用iterator就不会抛这个异常。抛不抛这个异常其实取决于调用的List的remove方法还是iterator的remove方法。我们来看看两个remove方法有什么不一样。

// List的remove方法
public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

// iterator的remove方法
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();
    
    try {
        ArrayList.this.remove(lastRet); //这里调用的是list的remove方法
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount; // 关键是这里
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
    }

iterator的remove方法里除了调用List的remove方法之后多了一个操作就是expectedModCount = modCount。然后有个checkForComodification方法是这样的

final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

foreach循环之所以会抛ConcurrentModificationException异常就是调用的是List的remove方法导致expectedModCount和modCount不一致。如果使用iterator遍历,使用的remove方法也是List的remove方法的话,也会出现同样的问题。

所有避免expectedModCount和modCount不一致导致的ConcurrentModificationException异常的方法就是使用iterator的remove方法。

当然,使用普通的for循环是不会抛这个异常的,因为这个异常不是List的remove方法抛出的。使用普通的for循环来删除元素也是有技巧的。

public static void main(String[] args) {
        List<Integer> lst = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5,6,7,8,9,10));
        for(int i = lst.size() - 1 ; i >= 0 ; i--) {
            System.out.println(lst.get(i));
            lst.remove(i);
        }
    }

上面的代码可以正常运行。并且输出和预料的一样是10,9,8,7...3,2,1

public static void main(String[] args) {
        List<Integer> lst = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5,6,7,8,9,10));
        for(int i = 0 ; i < lst.size() ; i++) {
            System.out.println(lst.get(i));
            lst.remove(i);
        }
    }

这个代码就不一样了,会输出1,3,5,7,9 原因就是删除1之后,2挪到了数组下标为0的位置,但是i已经变成1了,而1的位置已经不是2是3了,所以2就错过了。4,6,8,10也是同样的道理