关于java迭代器删除倒数第二个元素不会报错的原因的分析

关于java迭代器删除倒数第二个元素不会报错的原因的分析

1.前言

今天学弟来问我一个问题,在java的迭代器中,利用集合删除时会报错(java.util.ConcurrentModificationException),但是发现在删除倒数第二个元素的时候,就不会报错.我一时也没回答上来,后来查看了源码之后,有所理解,总结形成了这篇博客.

2.首先回答为什么删除倒数第二个不会报错

如图:

在这里插入图片描述

我们可以显然发现,在输出框里,只有0,1,2并没有3,那就说明在删除完2之后,在执行hasNext方法的时候,循环结束了,那么我们先去看hasNext方法的源码

hasNext()源码

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

显然可以发现,结束循环的原因是因为cursor和size()相等了,那么这两个值分别是什么呢?我们进一步了解

在这之前我们先了解一组继承关系

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E>

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

可以显然发现:ArrayList,AbstractList,AbstractCollection三者具有继承关系

然后我们查看源码,发现AbstractCollection中有这样一个方法:

public abstract int size();

而在这三个类里面,唯一实现了size()函数的方法就位于ArrayList中:

public int size() {
    
    
    return size;
}

而在ArrayList里面,size()的作用就是求集合的长度,至此,我们搞明白了hasNext()里size()的意思,就是当前集合的长度

那么cursor呢?

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();
    }
}

next就是当前的索引位置,那么根据next函数的代码,我们可以清晰的知道,在迭代器倒数第二个数的next函数执行完之后cursor的值为集合的长度-1,这个时候,我们再看最开始的测试demo,看我们执行了什么.

在这里插入图片描述

我们利用集合的remove函数删除了一个元素,那么这个时候,集合的长度-1,也就是size()的值-1,但是cursor的值没有变化

答案显而易见了,此时cursor和size()的值相同,循环结束,最后一个值没有输出,也没有报错.

如果到这里就解决了你的疑惑,你可以关闭本博文了,如果想进一步了解,就继续读下去.

3.进一步深入

通过前面的描述我们可以知道为什么删除倒数第二个不会报错了,但是我们仍然还有一个疑问,那就是删除其他位置的元素的时候那个报错的信息(java.util.ConcurrentModificationException)是从哪来的.

继续查看AbstractList源码,我们终于发现了这个错误的所在位置

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

在这里插入图片描述

同时,通过阅读报错的提示信息,我们也可以了解到,这个错误正是从next函数所在行报出的,还记得前面的next函数的源码么?

可以显然看到,next函数的函数体里第一句代码就是执行checkForComodification函数

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()源码

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

现在我们已经知道了错误是在这里产生的,是因为modCount和expectedModCount不相等的原因,那么modCount和expectedModCount又是什么呢?

通过继续查看AbstractList源码,我们可以找到modCount和expectedModCount

protected transient int modCount = 0;

int expectedModCount = modCount;

modCount是定义于AbstractList中的一个成员变量,而expectedModCount是定义于私有类Itr中的一个成员变量.同时在创建expectedModCount的时候就将modCount的值赋给expectedModCount,两者是相等的.

通过这里我们就更深的理解了为什么删除倒数第二个元素不会报错了,因为它避开了调用next函数,就避开了迭代器对于集合的检验,自然不会报错

通过对checkForComodification函数的分析,我们发现是我们破坏了expectedModCount和modCount相等关系导致了报错,那么我们进行了什么操作呢?

在这里插入图片描述

答案很显然,我们执行了集合的remove函数,是这个函数的执行破坏了平衡关系,让我们来看看这个remove执行了什么.

remove(Object o)源码

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;
}

我们是根据元素删除的,因此找到了这个函数,但是好像并没有什么发现,是不是哪里出问题了?

别着急,我们继续看源码,经过仔细观察,我们发现函数调用了fastRemove函数,那让我们继续查看fastRemove的源码

fastRemove(int index)源码

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
}

在看到这个函数的第一眼,我相信各位已经有了答案,不过我还要啰嗦几句,给那些没明白的小伙伴们.

我们在函数第一句里执行了modCount++,还记得我们前面的发现么, ArrayList,AbstractList,AbstractCollection三者具有继承关系,同时modCount还是AbstractList的成员变量.

此时我们明白了为什么checkForComodification函数报错了,因为我们执行了集合的remove(Object o)方法改变了modCount的值,导致modCount和expectedModCount不相等,从而导致了错误.至此关于这个问题的了解完美结束了.

4.后记

在遇到问题时打破砂锅问到底是个良好的习惯,我们对于问题的好奇也将引发我们更深的思考.

猜你喜欢

转载自blog.csdn.net/qq_45826803/article/details/120139738