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()方法上面的注释得出的结论的图供大家参考学习: