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

相关文章
|
1月前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
59 1
|
25天前
|
消息中间件 Java Kafka
在Java中实现分布式事务的常用框架和方法
总之,选择合适的分布式事务框架和方法需要综合考虑业务需求、性能、复杂度等因素。不同的框架和方法都有其特点和适用场景,需要根据具体情况进行评估和选择。同时,随着技术的不断发展,分布式事务的解决方案也在不断更新和完善,以更好地满足业务的需求。你还可以进一步深入研究和了解这些框架和方法,以便在实际应用中更好地实现分布式事务管理。
|
1月前
|
Java API 调度
如何避免 Java 中的 TimeoutException 异常
在Java中,`TimeoutException`通常发生在执行操作超过预设时间时。要避免此异常,可以优化代码逻辑,减少不必要的等待;合理设置超时时间,确保其足够完成正常操作;使用异步处理或线程池管理任务,提高程序响应性。
62 12
|
1月前
|
Java
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
68 9
|
1月前
|
Java
在 Java 中,如何自定义`NumberFormatException`异常
在Java中,自定义`NumberFormatException`异常可以通过继承`IllegalArgumentException`类并重写其构造方法来实现。自定义异常类可以添加额外的错误信息或行为,以便更精确地处理特定的数字格式转换错误。
34 1
|
5天前
|
Java 机器人 程序员
从入门到精通:五种 List 遍历方法对比与实战指南
小米是一位热爱分享技术的程序员,本文详细介绍了 Java 中遍历 List 的五种方式:经典 for 循环、增强 for 循环、Iterator 和 ListIterator、Stream API 以及 forEach 方法。每种方式都有其适用场景和优缺点,例如 for 循环适合频繁访问索引,增强 for 循环和 forEach 方法代码简洁,Stream API 适合大数据量操作,ListIterator 支持双向遍历。文章通过生动的小故事和代码示例,帮助读者更好地理解和选择合适的遍历方式。
21 2
|
1月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1月前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
1月前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
24天前
|
安全 Java 开发者
Java中WAIT和NOTIFY方法必须在同步块中调用的原因
在Java多线程编程中,`wait()`和`notify()`方法是实现线程间协作的关键。这两个方法必须在同步块或同步方法中调用,这一要求背后有着深刻的原因。本文将深入探讨为什么`wait()`和`notify()`方法必须在同步块中调用,以及这一机制如何确保线程安全和避免死锁。
37 4
下一篇
DataWorks