title: 查错ConcurrentModificationException date: 2017.12.12 22:18 categories:
- 技术博客 tags:
- J2SE
- 集合框架
本文阐述该异常排错的思路,主要涉及到J2SE中集合框架的基础内容,解决思路:上网查+理解JDK源码。
起因
某我手把手带学Java
的同学来问我这个问题:使用Iterator
迭代List
中,使用list.remove()
时报了ConcurrentModificationException
这个异常,原因是什么呢?我为什么不能删除集合中的某个元素呢?
应该要表扬他的表述能力,清晰表达了问题的上下文,也带出了他自己的疑虑,这点有助于我来定位问题。
经过
建议先阅读这篇cnblog上面的博客,如果你能懂,就没必要继续阅读本文。
首先这个问题需要简单了解涉及到的API
,即各个类与内部使用到的方法实现。此例中可查看:
ArrayList
Itr
(ListItr
同理)
其实都在一个类中,后者是前者的内部类,此处的内部类是一种代码高内聚的封装手法,在阅读源码时,注意力主要集中在两个成员上:
AbstractList
中的protected transient int modCount = 0
负责记录集合元素修改的次数,而ArrayList
继承了这个抽象类,因而是直接的成员变量Itr
中的int expectedModCount = modCount
负责记录迭代器修改元素的次数
注意他俩的初始值,前者是0,后者从前者来。
对ArrayList
与Iterator
之间的关系,用大白话讲,一个负责装东西,一个负责数东西,所以前者是一个容器,后者是一个计数器(模拟cursor
)。好比是,前者是跳绳子的人,后者是数跳了多少下的人。
那么,自己跳多少次跟帮你计数的人的结果是否应该相等呢?
逻辑上应该是相等的,JDK代码中也是这么做的,所以后者初始化成员时要从前者赋值。
而在迭代器的修改元素的方法中,首先要调用checkForComodification()
来确保这个逻辑一致,实现上就是比较两个count数值。
理解到了这一步,我们回头看下报错的场景,此时使用的不是Itr
中的remove()
而是ArrayList
中的。
这就好比,跳绳子的人之前一直是匀速跳,某一秒慢了一倍速,结果上跳绳次数就应当-1,但数的人并没发觉,逻辑上,他下一次数的时候,已经是错误的计数值了。体现到代码中,就是抛出异常。
在代码中,list.remove()
从数组中去掉了某元素,modCount
也-1,但因为此时使用了迭代器遍历,而Itr
即迭代器实现类中的expectedModCount
并没有-1,二者不等:
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
复制代码
简言之:使用迭代器遍历list
时,会创建一个集合线程+一个计数线程,每次开始前会检查二者对应的修改次数是否等,不等即报错。
引申:Java
中foreach
在遍历时,底层同样使用了Iterator
来计数,所以应当注意不使用list.remove()
,正确的,应该使用iterator.remove()
。关于这点,C#
中更加清晰,可以了解下C# 运算符重载。
小结
- 命名可读性:本例中涉及到的JDK源码从命名上极其具有可读性,即使不清楚业务实现,根据名字也会猜测到用意,如
ConcurrentModificationException
正翻译为并发修改异常,与我们上述结论相符。 - 理解类中成员的作用:成员包括变量与方法,前者存储数据状态,后者根据业务场景更新状态。
- 排错的好心态与清晰的思路:报错解决掉后就可以提高一点个人能力,同时过程中要充分利用工具、资源。