今天有朋友突然在群里抛出一句,”java中使用foreach遍历时,为啥不让删除元素呢?设计ConcurrentModificationException的意义是什么目的呢?如果单线程操作,还需要吗?” 。今天我们就来聊一聊这件事。
如果在使用 Iterator
遍历一个元素的时候,如果同时使用 List.remove()
方法去移除元素,会报出 ConcurrentModificationException
异常。如何避免发生这种异常,以及如何为什么会抛出这个异常。
先上代码:
1 | public static void main(String[] args) { |
异常日志:
1 | Exception in thread "main" java.util.ConcurrentModificationException |
看看源码
我们看上面的代码貌似没有使用 Iterator
呀,其实上面的 foreach
循环就是其实就是使用了这个对象,甚至如果你使用这样的语句 System.out.print(list)
也会使用的list
对象的迭代器。我们把上面的代码改写一下
1 | public static void main(String[] args) { |
iterator.next()
究竟在哪里抛出了异常,我们来一探究竟。首先 iterator
是 ArrayList.Itr
类型,next 方法源码如下
1 | public E next() { |
1 | final void checkForComodification() { |
到这里我们明白,抛出异常的原因是因为 modCount != expectedModCount
,但是这两个字段是什么含义呐,为什么会不相等?
modCount
是AbstractList
的一个字段,用来表示这个容器实例被修改的次数,如果容器中的元素有增加、移除、替换等操作的都会修改这个值。expectedModCount
是 ArrayList.Itr
类的一个字段。在创建迭代器的时候,会将modCount
赋值给expectedModCount
1 | private class Itr implements Iterator<E> { |
然后我们来结合我们的代码来分析,在循环中我们移除了元素值为 7 的元素 list.remove(next);
在该方法内部调用了 fastRemove
方法
1 | /* |
等到到达下一次 Integer next = iterator.next();
的时候就触发了 ConcurrentModificationException
异常。
以上,对于异常的原因分析就结束了。
如何避免这个异常
改写上面的代码,利用 iterator
来移除元素即可。
1 | public static void main(String[] args) { |
我们来看看 iterator.remove();
发生了什么
1 | public void remove() { |
为什么
单线程的影响
看到这里你可能会问,如此大费周章的去删除一个元素究竟是为了什么?
通过两种移除元素方法的对比可以发现,使用iterator.remove();
移除元素,仅仅支持移除当前迭代的元素,并且在不进入下一次迭代前iterator.remove();
只能调用一次。而通过 list.remove
的方式可以移除任意的元素。通过使用迭代器移除可以获得一个容器确定的视图。下面我们假装list.remove
不会抛出异常,来举个例子
1 | public static void main(String[] args) { |
我们本来是想拿到一个完整的迭代,但是却缺少了 7 这个元素。这里仅仅是做了打印处理,如果是要利用迭代器计算这个容器所有元素的和,那么这个和必然是不符合预期的。有人会说我自己知道移除了 1 这个元素,并且所以我预期的和就是没有 7 的和。如果真的是这样的话,这样的代码维护起来将是一个噩梦,你要注意在哪里移除了某个元素,以及是否会对后面的逻辑产生影响。当代码逻辑进一步复杂的时候,这样的做法会让代码表现更加的不可预期。
多线程的影响
如果是在多线程中使用 ArrayList
,仅针对本文中涉及的元素来看,modCount
的可见性会有问题,每个线程看到的modCount
的大小可能是不一样的,同时modCount++
等改变值得操作也会没有同步性措施而变得失去原子性。
Vector
作为同步版本的 List
的做法是在有关 modCount
操作的地方使用 synchronized
来保证可见性和同步。
由于 list.iterator();
每次都会生成新的迭代器,所以 cursor
和 lastRet
变量是线程封闭的,无需同步。