List、Map等这类数据结构在日常开发中的使用不可谓不多,经常会有遍历的同时进行修改的情况。例如:
1 | Integer[] numbers = new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; |
这段代码在执行的时候便会抛出异常:
1 | Exception in thread "main" java.util.ConcurrentModificationException |
这个异常出现通常是遍历一个集合的同时,在修改这个集合。看到next
方法:
1 | public E next() { |
然后看到checkForComodification
:
1 | final void checkForComodification() { |
即当modCount 与 expectedModCount 不相等时,会抛 ConcurrentModificationException 异常
。其实很早之前就会碰到这个问题,使用Iterator
进行遍历和操作就不会出现这个问题了,但是一直没有深究其原因。
之前听同事说,for each
内部就是使用的Iterator
。但是最近项目中就算使用了for each
也还是出现了上面的异常。于是决心细究一番。
将上面的示例代码转成 class 文件,可以看到:
1 | Integer[] numbers = new Integer[]{Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4), Integer.valueOf(5), Integer.valueOf(6), Integer.valueOf(7), Integer.valueOf(8), Integer.valueOf(9), Integer.valueOf(10)}; |
确实看到了Iterator
的身影,但是却忽视了真正重要的一点:remove()方法的执行者
。
将遍历代码改成如下:
1 | Iterator iterator = list.iterator(); |
便可执行通过了。看到 class 文件如下:
1 | while(iterator.hasNext()) { |
可以看到都是通过Iterator
进行遍历,唯独在执行remove
操作的时候,报错的示例执行对象是list本身
,而正确的示例执行对象是iterator
。那么接下来对比下二者的remove
方法,便可找到真相了。
看到ArrayList
自身实现的remove
方法:
1 | public boolean remove(Object var1) { |
看到ArrayList中Iterator
实现的remove
方法:
1 | public void remove() { |
如此便一目了然了,ArrayList 本身的 remove 方法执行完之后没有同步 modCount 与 expectedModCount,而 Iterator 有同步。在遍历的时候会 checkForComodification,当使用的非 Iterator 的 remove 方法,会造成 2 个 count 不相等,如此便会抛出异常了。
那么细想一下:为什么 Java 集合在遍历的时候要做这样的检查呢?这里引用一下网友的想法,大家自行思考吧~
设置modCount和expectedModCount的目的是为了检测iterator的有效性,检测是否有其它操作对HashMap的结构进行了修改,由于这些操作不是通过当前iterator进行的,因此有可能破坏iterator的有效性。通过iterator执行remove只能删除当前iterator所在的元素,不会让iterator失效。而通过HashMap.remove()实际上可以删除任意元素,这个元素有可能正是iterator内部的next变量已经引用了的元素,造成iterator失效。
最后说 2 点题外话:
Arrays.asList
不接受 Java 基本数据类型数组作为参数。原因asList
接受的参数为Object
,而基本数据类型不是。但是代码List list = Arrays.asList(new int[]{1,2,3})
不会报错,因为int[]
是Object
,此代码会生产一个size = 1
的列表。- 通过
Arrays.asList
方法返回的List
是不能修改的。
参考:
Java遍历HashMap并修改(remove)
把Java数组转换为List时的注意事项
Java 集合细节(二):asList 的缺陷