ArrayList是java开发时非常常用的类,常碰到需要对ArrayList循环删除元素的情况。这时候大家都不会使用foreach循环的方式来遍历List,因为它会抛java.util.ConcurrentModificationException异常。比如下面的代码就会抛这个异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
那是不是在foreach循环时删除元素一定会抛这个异常呢?答案是否定的。
见这个代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这段代码和上面的代码只是把要删除的元素的索引换成了4,这个代码就不会抛异常。为什么呢?
接下来先就这个代码做几个实验,把要删除的元素的索引号依次从1到5都试一遍,发现,除了删除4之外,删除其他元素都会抛异常。接着把list的元素个数增加到7试试,这时候可以发现规律是,只有删除倒数第二个元素的时候不会抛出异常,删除其他元素都会抛出异常。
好吧,规律知道了,可以从代码的角度来揭开谜底了。
首先java的foreach循环其实就是根据list对象创建一个Iterator迭代对象,用这个迭代对象来遍历list,相当于list对象中元素的遍历托管给了Iterator,你如果要对list进行增删操作,都必须经过Iterator,否则Iterator遍历时会乱,所以直接对list进行删除时,Iterator会抛出ConcurrentModificationException异常
其实,每次foreach迭代的时候都有两步操作():
- iterator.hasNext() //判断是否有下个元素
- item = iterator.next() //下个元素是什么,并赋值给上面例子中的item变量
next()方法的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
这时候你会发现这个异常是在next方法的checkForComodification中抛出的,抛出原因是modCount != expectedModCount
- modCount是指这个list对象从new出来到现在被修改次数,当调用List的add或者remove方法的时候,这个modCount都会增加;
- expectedModCount是Iterator类中特有的变量,指现在期望这个list被修改的次数是多少次,这个值在调用list.iterator()创建iterator的时候初始化为modCount,该值在iterator初始化直到使用结束期间不会改变。
iterator创建的时候modCount被赋值给了expectedModCount,但是调用list的add和remove方法的时候不会同时修改expectedModCount,这样就导致下次取值时检查到两个count不相等,从而抛出异常。
解决这个问题的一种方式是使用Iterator来操作列表:
1 2 3 4 5 6 |
|
那么为什么这种方式不会抛出该异常呢?下面是ArrayList中内部类Itr的remove方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
注意下面这句:
1 |
|
可以看出,在使用iterator()方法得到的Iterator对象后,通过iterator.remove方法是可以正确删除列表元素的,因为它保证了expectedModCount=modCount。
避免这个问题的另一种方法,是不使用foreach语句的for循环:
1 2 3 4 5 6 7 8 |
|
回到问题上来,在使用foreach迭代ArrayList时,是可以删除任何一个元素的,且只能删除一个,而且这只能发生在迭代到倒数第二个元素的时候。比如下面的代码不会有异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
其真正的原因是remove("5")这一句之后,下一次foreach语句将调用iterator.hasNext()方法,如果此时返回false,这样就不会进到next()方法里了,也就不会调用checkForComodification而导致异常了。
疑问:当循环到倒数第二个元素时,如果再多删除一个会怎样呢?比如:
1 2 3 4 5 6 7 |
|
这段代码中,list是可以被打印出来的,因为list.remove()方法可以正确执行,其结果也是正确的。但是执行完这次打印,进入下一次迭代时,又产生了checkForComodification异常,还没想明白为什么。如果哪位大牛知道,请留言。