ArrayList之ConcurrentModificationException异常源码分析

  • 首先,出现该异常的代码:
List<String> strs = new ArrayList<>();
        strs.add("1");
        strs.add("2");
        strs.add("3");
        strs.add("4");
        strs.add("5");
        for (String item : strs) {
                    if (item.equals("3")) {
                        strs.remove(item);
                    }
                }

执行上面这段代码是会抛出ConcurrentModificationExcepiton异常的,至于为什么我们待会儿在源码的分析当中再给出答案。同样我们再来看下面这段代码:

List<String> strs = new ArrayList<>();
        strs.add("1");
        strs.add("2");
        strs.add("3");
        strs.add("4");
        strs.add("5");
        for (int i = 0; i < strs.size(); i++) {
            String str = strs.get(i);
            if (str.equals("3")) {
                strs.remove(str);
            }
        }

而再执行这段代码的时候是不会抛出ConcurrentModificationException的,同样是for循环,差别在哪里?


我们知道,第一种方法里面用到的是foreach循环,而foreach循环的具体实现是Iterator,每次循环的时候都会调用Iterator中的hasNext()方法,该方法返回为true则调用next()方法获得该元素,这就是这两段代码产生不同结果的原因。那么为什么使用了Iterator就抛出该异常呢?这时候如果你想起另外一种实现遍历List的方式,可能就会对我所说的使用了Iterator就会抛出异常产生疑问。直接上代码

List<String> strs = new ArrayList<>();
        strs.add("1");
        strs.add("2");
        strs.add("3");
        strs.add("4");
        strs.add("5");

        Iterator<String> iterator = strs.iterator();
        while (iterator.hasNext()) {
            String next = iterator.next();
            if (next.equals("3")) {
                iterator.remove();
            }

        }

当我们再次运行上面这段代码的时候,发现是能正常删除集合中等于“3”的这个元素的,那么问题来了,刚才也说到了,使用foreach来循环List并且删除指定的某个元素时会抛出异常,并且其内部实现也是Iterator,那么它与第三种实现方式的差别到底在哪儿?


当我们分析到这儿的时候,我们可以得出这样一个结论,当我们使用到Iterator的时候ConcurrentModificationException异常才有可能会发生,那么我们从源码找答案的时候就直接从集合的迭代器Iterator中来查找。以ArrayList为例:


ArrayList.iterator()方法得到的实例为class Itr,Itr是ArrayList的内部类。我们只关注Itr中的hasNext()、next()、remove()、checkForComodification()这几个方法。

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        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];
        }

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

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        final void checkForComodification() {
            if (modCount != expectedModCount) // 注意此处,这是抛出异常的原因
                throw new ConcurrentModificationException();
        }
    }

在初始化Itr的时候,会将modCount的值赋给expectedModCount,其中modCount为The number of times this list has been structurally modified,每当调用ArrayList的add()、remove()等方法的时候都会执行modCount++。
回到我们用foreach循环List的代码中,当调用strs.remove(item)后,这时modCount会加1,而expectedModCount的值没变,接着会调用hasNext()方法检查是否已经执行到最后一个元素,在上述代码中显然还不是最后一个元素,所以接着执行next()方法,注意,进入next()方法后会先执行checkForComodification()方法,而该方法只做了判断modCount与expectedModCount是否相等这一件事,从前面的分析可以看出来这时候两者的值是不相等的,所以抛出ConcurrentModificationException。


那使用Iterator来迭代ArrayList的时候调用Iterator.remove()方法的时候为什么就没有抛出异常呢?答案就在于调用Iterator.remove()方法除了会调用ArrayList中的remove()方法外,还会将改变后的modCount值再次赋给expectedModCount,使得两者再次相等,从而在下次调用checkForComodification()方法检查时不会抛出异常。


还有一个小的知识点,就是在第一段代码的foreach方法中,如果我们删除的是倒数第二个元素,是不会抛出异常的。那么这又是为什么呢?前面已经提过,foreach方法是Iterator来实现的,这时我们再次查看Itr中的代码。next()方法中获取下一个元素其实是通过不断的向后移动游标cursor,从而在数组中取得对应的元素(ArrayList的底层实现仍然是数组)。游标cursor的移动过程点击查看


在这里我们主要关心当取到倒数第二个元素,即图中角标为5的元素的时候,这时cursor的值为6。取到值后,接着会调用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++; // 对ArrayList modify的次数,每当调用add()、remove()方法都会加1
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved); // 该方法将数组中最后一个元素的值赋给倒数第二个元素,没有改变数组的大小,数组大小为7。
        elementData[--size] = null; // 关键点在这儿,这时会将数组中的最后一个元素的值设置为null,并且将size减1,size表示的是集合的大小,这时候size的值为6.
    }

当成功删除倒数第二个元素的时候,Iterator会先调用hasNext方法查看是否还有未取完的元素,正常情况选,应该继续读取最后一个元素,但是从上面的分析中知道,这时候的size值为6,cursor的值也为6,而hasNext方法返回tre的条件是两者不相等,所以程序走到这里就退出了foreach循环。


分析到这里,对于为什么删除倒数第二个元素不会抛出ConcurrentModificationException异常的原因已经非常明显,因为这时没有遍历到集合中的每一个元素,所以这种写法也是不正确的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值