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

简介: 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,此时将抛出异常:


微信图片_20220111192555.png


2、前戏知识:

subList()方法原理分析:


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


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


微信图片_20220111192612.png


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


微信图片_20220111192630.png

通过源码可以看到,这个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()方法:微信图片_20220111192659.png


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


微信图片_20220111192703.png


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


微信图片_20220111192800.png


由此可见: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();这个方法:


微信图片_20220111192818.png


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


微信图片_20220111192834.png


重点来了,这个方法里面首先判断了 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()方法上面的注释得出的结论的图供大家参考学习:


微信图片_20220111192851.png

相关文章
|
2天前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
|
2天前
|
Java
在 Java 中,如何自定义`NumberFormatException`异常
在Java中,自定义`NumberFormatException`异常可以通过继承`IllegalArgumentException`类并重写其构造方法来实现。自定义异常类可以添加额外的错误信息或行为,以便更精确地处理特定的数字格式转换错误。
|
6天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
6天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
8天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
3天前
|
IDE 前端开发 Java
怎样避免 Java 中的 NoSuchFieldError 异常
在Java中避免NoSuchFieldError异常的关键在于确保类路径下没有不同版本的类文件冲突,避免反射时使用不存在的字段,以及确保所有依赖库版本兼容。编译和运行时使用的类版本应保持一致。
|
5天前
|
Java 编译器
如何避免在 Java 中出现 NoSuchElementException 异常
在Java中,`NoSuchElementException`通常发生在使用迭代器、枚举或流等遍历集合时,尝试访问不存在的元素。为了避免该异常,可以在访问前检查是否有下一个元素(如使用`hasNext()`方法),或者使用`Optional`类处理可能为空的情况。正确管理集合边界和条件判断是关键。
|
9天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
42 4
|
7天前
|
Java
Java异常捕捉处理和错误处理
Java异常捕捉处理和错误处理
11 1
|
9天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
20 2