浅析List中remove导致的异常

本文探讨了在Java中使用foreach循环时,从ArrayList中移除元素导致的异常原因。通过分析源码,揭示了ArrayList的Fail-Fast机制以及modCount和exceptedModCount的作用。在循环中移除元素时,建议使用Iterator并适当加锁以避免并发问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题起因

闲来无事看起了java开发手册,发现一个有意思的地方,我整理了一下,大概如下:

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

手册说禁止使用上面的方法,我就很奇怪,没毛病呀,为啥呢,一运行报错了。我先尝试了“1”情况,输出2很正常,但是变为2的时候,输出如下:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at Main.main(Main.java:10)

为什么会这样呢,我就去研究了一下。

分析过程

先查看哪里出错了呢!找到了位置如下:

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

显然是modCount与exceptedModCount不同造成的,然后对两个参数分析。
分析modCount,发现这个变量是继承父类AbstractList而来,默认值为0。

protected transient int modCount = 0;

可是这个变量什么时候尽心改变的呢?这个时候需要追本溯源了,为啥会比较两个值,是因为ArrayList即从List中继承了iterator(),又从AbstractList中继承了list。
为什么呢,是为了实现ArrayList的Fail-Fast机制。Fail-Fast机制的作用是避免ArrayList在迭代过程中数组结构发生变化的问题。
所以会进行两个数据的判断,查看是否改变了数据结构。查看一下AbstractList中的iterator()方法。
idk1.8如下:

public Iterator<E> iterator() { return listIterator(); }
只是返回了一个Iterator的数据结构,在进一步查看listIterator()方法

    public ListIterator<E> listIterator() {
        return listIterator(0);
    }

调用了listIterator(0)这个方法,注意传入了新的参数,不是之前的那个方法了,进一步剖析!!发现下面一个方法只是先判断是否数组下标越界,然后返回了一个新的数据结构。

 public ListIterator<E> listIterator(final int index) {
        rangeCheckForAdd(index);

        return new ListItr(index);
    }

    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 {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

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

这里省略了一步,他继承了Itr,直接分析Itr容易理解。重要是对上面的四个值进行变化分析:

代码拆分理解:

		int cursor = 0;
        int lastRet = -1;
        int expectedModCount = modCount;

cursor:表示下一个要访问的元素的索引,从next()方法的具体实现就可看出

lastRet:表示上一个访问的元素的索引

expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。

modCount是AbstractList类中的变量,前面分析到过,该值表示对List的修改次数
  
接着是hasNext()方法,判断访问是否还有未访问的值!!!!
public boolean hasNext() { return cursor != size(); }

紧接着执行next,可能有人问,你是foreach呀,为啥会next呢,对ArrayList进行遍历的时候,其实就是执行的next,因此分析next!!!

public E next() {
            checkForComodification();
            try {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

checkForComodification()方法就是之前说,判断数据结构是否发生改变的,也就是进行比较查看这中间当前的list中的数据是否发生了改变。每次增加修改等操作都会对modCount进行+1操作,exceptedModCount初始值就是modCount。

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

继续分析next方法:现将下一个要访问的元素用i进行存储,然后获取下一个元素next,最后一次操作的元素变为i,下一个元素跟新为i+1,然后返回要操作的元素next。此时最值得注意点就是,modCount与exceptedModCount的值相等,都为0。
然后执行remove方法,调用了ArrayList中的remove()方法:

public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    private void fastRemove(int index) {
        modCount++;
        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
    }

较为关键在每次remove方法都会遍历当前数据,判断是否存在,如果存在调用fastRemove方法,而这里面 modCount++是第一步操作 ,再操作之前modCount就为1了,而此时执行判断时,两者不想等,自然会报错。我们在分析一下为什么第一个不会报异常,看源码可以看出,倒数第一个元素的时候,Itr的cursor是1,当list删除掉一个元素的时候,size也是等于1,所以在判断的时候,cursor==size,因此就会在删除了一个元素以后,直接跳出循环,所以不会报异常了。

总结

foreach遍历内部也就是与iterator一致,实例化为iterator对象。从之前的源码可以看出,在remove的过程中,modCount会改变,因此在一个循环中我们不要同时采用两种方法。如下:

ArrayList<String> list = new ArrayList<>();
        list.add("2");
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            String s= iterator.next();
            if(s.equsls("2"))
                list.remove(s);
        }

这样是与之前的foreach循环属于同样的情况,值得注意。另外,

不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。

上面的正确写法如下:

        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String item = iterator.next();
            if (item.equals("2")) {
                iterator.remove();
            }
        }

参考:阿里巴巴Java开发手册终极版v1.3.0.pdf

积土成山,风雨兴焉!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值