本文主要从源码的角度分析 ConcurrentModificationException 发生的原因,以及解决办法。
需求与实现
需求:从一个集合中找出指定的元素并将其删除。
问题代码:
1 | ArrayList<Integer> list = new ArrayList<>(); |
异常堆栈:
1 | Exception in thread "main" java.util.ConcurrentModificationException |
原因排查
从异常的堆栈信息中可以看出,在 ArrayList 的 forEach() 方法中抛出异常,那就先看一下这个方法。
1 | public void forEach(Consumer<? super E> action) { |
经过对 forEach() 方法的源码分析,发现 modCount != expectedModCount 会导致 for 循环退出,并且抛出 ConcurrentModificationException。
从第 3 行代码 final int expectedModCount = modCount
中可以知道,初始状态 expectedModCount 和 modCount 的值是相等的,那么是什么原因导致它们的值不相等进而抛出异常的呢?
原因是 ArrayList 的修改操作(remove 或 add)会导致 modCount 的值发生变化。这里以 remove 方法为例进行分析证明:
1 | public boolean remove(Object o) { |
读完上面的源码会发现 remove 方法并没有修改 modCount 的值啊。别着急,再仔细观察会发现无论怎样,最终会执行 fastRemove 方法。再来看一下这个方法的实现:
1 | private void fastRemove(int index) { |
这次没错了吧。
所以在 forEach() 方法中使用 ArrayList.remove() 删除元素会抛出异常的原因是:由于 ArrayList 执行 remove 方法后,modCount 的值加 1,当 forEach 中的循环检查两个条件时,会因 modCount == expectedModCount 不成立导致 for 循环退出,进而抛出 ConcurrentModificationException 异常。
由此可见,forEach 循环只适用于对 ArrayList 的遍历,但是不可以对 ArrayList 进行添加或删除操作,否则将抛出异常。
现在问题的原因找到了,那么我们应该怎么解决呢?
解决办法
方法一:使用普通的 for 循环
1 | ArrayList<Integer> list = new ArrayList<>(); |
运行代码,发现没有抛出异常。问题解决!
但是,如果需求改为删除所有元素呢?
1 | ArrayList<Integer> list = new ArrayList<>(); |
先猜一下运行结果是什么,再往下看。
——– 这里是分隔线(这里先插播一条广告,广告之后更加精彩!哈哈!!!)————
你还在为开发中频繁切换环境打包而烦恼吗?
快来试试 Environment Switcher 吧!
使用它可以在app运行时一键切换环境,而且还支持其他贴心小功能,有了它妈妈再也不用担心频繁环境切换了。
https://github.com/CodeXiaoMai/EnvironmentSwitcher
目前该项目已经达成如下成就:![]()
![]()
——————- 这里是分隔线(广告结束,精彩继续!!!)——————–
好了,回归正题你猜出结果了吗?看看你猜的对下对。
运行结果为:
1 | 执行完毕 list.size() = 5 |
咦?明明是遍历 list 的每个元素并把它们删除,为什么最后还有 5 个元素呢?
这是因为 ArrayList 内部使用数组存储数据,10 个元素依次存储在数组的第 0 个位置开始到第 9 个位置,当中间有一个元素删除时,后面的所有元素会向前移动,保证中间没有空余。所以当第 0 个位置的元素删除完毕后,本来在第 1 个位置的元素变成第 0 个元素,但由于 i++ 后 i = 1,下次执行删除操作时会删除最新的第 1 个位置的元素,而第 0 个位置的元素因被跳过而不会被删除,同理当后面的元素被删除时也会有跳过,这就是为什么最终会有 5 个元素的原因。
所以如果只是在遍历 ArrayList 的同时删除指定的元素后并退出循环,这样是没有问题的,因为删除元素后,不会进行其他操作。但是如果删除所有元素,就需要做一些特殊的处理了,具体处理如下:
1 | for (int i = 0; i < list.size(); i++) { |
方法二:使用 Iterator
1 | ArrayList<Integer> list = new ArrayList<>(); |
如果要删除所有元素呢?
1 | ArrayList<Integer> list = new ArrayList<>(); |
运行结果:
1 | 执行完毕 list.size() = 0 |
从运行结果可以知道,程序正常运行而且完全符合预期。为什么使用 Iterator 可以正常遍历并且删除所有元素呢?下面从源码的角度分析:
首先看,list.iterator()
的实现:
1 | public Iterator<E> iterator() { |
这个方法创建并返回一个 Itr
类的实例。通过查看源码可以发现 Itr 实现了 Iterator
接口,这不正是我们需要的吗?接下来依次看 iterator.hasNext()、iterator.next() 以及 iterator.remove() 方法:(前方高能,请注意准备好脑子!!!)
1 | private class Itr implements Iterator<E> { |
总结:
使用 List.iterator() 遍历集合的同时进行 iterator().remove() 操作,不会发送异常的原因是:Iterator.remove() 会在执行 ArrayList 的 remove() 后,重新将 expectedModCount 的值更新为 modCount 的值。这样运行 checkForComodification() 方法时,才会正常运行。
方法三:使用增强 for 循环
增强 for 循环是迭代器的简化书写格式,和 iterator 遍历的效果是一样的,也就说增强 for 循环的内部也就是调用 iterator 实现的,只不过获取迭代器由 jvm 完成,不需要我们获取迭代器而已。但是增强 for 循环有些缺点,例如不能在增强 for 循环里动态的删除集合内容(虽然内部也使用了 Iterator 但在删除时因为拿不到 Iterator,所以不能通过 Iterator 删除)、不能获取下标等。
所以这种方式只适用于删除一个指定元素后,立即退出循环的情况。
1 | ArrayList<Integer> list = new ArrayList<>(); |
总结
经过上面对异常发生的原因以及 3 种解决办法的分析,可以得出以下结论:
只要是在遍历时直接通过调用 ArrayList.remove() 移除元素都是不安全的,普通的 for 循环和增强 for 循环适用于删除一个元素后就退出循环体;而 forEach() 循环是坚决不可以执行删除操作的(因为它是程序员无法控制退出循环的),否则会抛出异常。最安全的方法是使用 Iterator 进行遍历的同时,并且必须使用 Iterator 提供的 remove() 方法进行删除操作。
扩展
上面分析是 remove() 操作,那 add() 操作呢?
按照思路应该也是使用 Iterator 的 add 之类的方法,但是当我们写代码的时候会发现,唉?唉?唉?唉?我 iterator 点,iterator 点…… 为什么没有点出来 add 之类的方法?
这是因为 Iterator 接口只提供了 remove() 方法,F***K,这可让我如何是好?别急,List 接口还提供了一个 listIterator() 方法,这个方法返回的对象是可以进行 add 操作的,我们来看一看下面的代码:
1 | ArrayList<Integer> list = new ArrayList<>(); |
运行结果:
1 | 0123456789 |
通过查看源码,可以发现 list.listIterator()
方法如下:
1 | public ListIterator<E> listIterator() { |
这个方法返回一个 ListItr 类的实例,并且这个类实现了 ListIterator 接口。
ListIterator 是一个在继承了 Iterator 接口的同时又额外提供了 add() 、set() 等方法的接口,如下图。
再看一下 ListItr 这个类:
1 | private class ListItr extends Itr implements ListIterator<E> { |
ListItr 不但实现了 ListIterator 接口,而且继承了 Itr 类(上面已经分析过),从源码中可以发现 ListItr 内没有实现 remove() 方法,显然这是直接复用它的父类 Itr 的 remove() 方法。
所以,如果在遍历的同时只是进行 remove 操作,既可以使用 iterator 也可以使用 listIterator;而要进行 add 操作,就必须使用 listIterator 了。
LinkedList 和 ArrayList 略有不同,虽然它的 iterator() 和 listIterator() 方法返回的接口类型不同,但是仔细分析源码会发现其实内部都是 ListItr 对象。