List中subList方法抛出异常java.util.ConcurrentModificationException原理分析

1、首先从测试代码开始:

public class Test {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0;i<6000;i++){
            list.add(i);
        }
        List<Integer> list1 = list.subList(0,3000);
        List<Integer> list2 = list.subList(3000,6000);
        list2.clear();
        System.out.println("list1 = " + list1);
 
    }
}

首先初始化一个6000个元素的list,然后,利用list.subList()截取3000个元素到list1中,再取出后3000个元素到list2中,然后清空list2,最后再打印list1,此时将抛出异常:

2、前戏知识:

subList()方法原理分析:

上面的测试方式为什么会出现这个情况,看上去明明没有任何问题,但是打印list1的时候就抛出异常,肯定不可能是System.out.println()有bug吧,再来仔细看看代码,似乎只有打印语句前面几句话会出现问题,那么就是subList()的调用以及clear()这几句代码了,那么问题到底出现在哪里,我们来一探究竟;

接下来我们首先看一下ArrayList中对subList()方法的实现的源码,看它究竟干了些什么事儿:

在subList()方法的源码中首先调用了 subListRangeCheck(fromIndex, toIndex, size) 这个方法主要作用就是判断subList()传入的参数是否合规,这里不是重点,重点在于它  return new SubList(this, 0, fromIndex, toIndex),返回了一个SubList对象,继续往下看一下这个SubList对象,源码在1010行:

通过源码可以看到,这个SubList对象是一个内部类,

2.1、在构造对象时,会传入4个参数:

AbstractList<E> parent:当前调用subList()方法的list对象

int offset:偏移量(从0开始)

int fromIndex:开始下标(包含)

int toIndex:结束下标(不包含)

2.2、在构造器内部:

将传入的parent赋给SubList对象的成员变量parent;

fromIndex赋给SubList对象的成员变量parentOffset;

offset+fromIndex赋给SubList对象的成员变量offset,用于记录元素的偏移量;

toIndex - fromIndex赋给SubList对象的成员变量size,用于记录此时会返回的数据量大小;

最后一个是 ArrayList.this.modCount 赋给SubList对象的成员变量modCount ,这个赋值比较关键,记录了修改过的次数,默认为0;

到这里,构造一个SubList对象就完成了,你可能会有疑问,只是单纯的构造了一个SubList对象,那么是怎么进行赋值取值的;解决这个问题,来看一下SubList对象的get()方法:

在get()方法中,最终返回的是 ArrayList.this.elementData(offset + index);可以看到,它是从当前的ArrayList对象中维护的一个elementData()方法中取值,再来看elementData()这个方法:

返回的是elementData这个数组中的元素:

由此可见:SubList对象中操作的集合与原始list中操作的集合是同一个集合,通过offset偏移量加上index来标记元素的位置;所以,当你操作原始list或者截取元素后生成的list1集合,都是影响同一个集合。

3、高潮部分:

异常产生分析:

有了上面第二步的分析,有了一个基本认识,那就是list.subList()方法返回的集合会直接影响原始的list集合,接下来继续分析java.util.ConcurrentModificationException异常出现的原因;

再次回到测试代码的以下四句代码:

List<Integer> list1 = list.subList(0,3000);
List<Integer> list2 = list.subList(3000,6000);
list2.clear();
System.out.println("list1 = " + list1);
首先通过  List<Integer> list1 = list.subList(0,3000); 等到一个list1; 

然后再次通过  List<Integer> list2= list.subList(3000,6000); 等到一个list2; 

然后清空list2 即list2.clear();

最后打印:System.out.println("list1 = " + list1);

由于上面分析我们知道,list2调用clear()方法,那么此时原始list维护的底层elementData数组势必会受影响,具体就是会把这后面3000个元素给删除掉,此时list1再去打印,它会调用自己重写的迭代方法iterator()进行遍历,然后调用父级AbstractList的listIterator()方法,由于SubList类继承了AbstractList 所以它会来调用SubList类的listIterator(final int index)方法,此时该方法内部在第一句就调用了checkForComodification();这个方法:

接下来看 checkForComodification()这个方法在干什么:

重点来了,这个方法里面首先判断了 ArrayList.this.modCount 与 this.modCount(即SubList的modCount)是否相同,如果不相同则抛出异常java.util.ConcurrentModificationException,写得累死我了,绕了一大圈终于写到这个异常了,在生成list1时,它在实例化一个SubList对象时将原始list的modCount赋值给了SubList对象,此时是默认值0,当list2.clear()时,原始list的modCount已经发生了变化,即不再是0,所以 此时打印list1时,checkForComodification()方法中的ArrayList.this.modCount != this.modCount判断肯定时true,所以这就是异常抛出的原因。

4、附上一位研究了subList()方法上面的注释得出的结论的图供大家参考学习:

猜你喜欢

转载自blog.csdn.net/feng8403000/article/details/114797903