《阿里巴巴Java开发手册》第一章里的第五节的第七点是这么说的:
【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
里面举了这样一个反例:
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); for (String item : list) { if ("1".equals(item)) { list.remove(item); } } 复制代码
其实Java的 forEach写法内部就是迭代器 ,大家可以把上面的代码理解为以下代码:
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if ("1".equals(item)) { list.remove(item); } } 复制代码
有了这一层理解后,那我们以ArrayList为例,看看其内部的 iterator
方法:
public Iterator<E> iterator() { return listIterator(); } public ListIterator<E> listIterator(final int index) { checkForComodification(); rangeCheckForAdd(index); final int offset = this.offset; return new ListIterator<E>() { hasNext()... next()... ... } } 复制代码
由于 listIterator()
方法内的内部类 ListIterator
的代码太多,我就不一一贴出来了,因为我们重点只看两个方法: hasNext()
和 next()
,接下来我会通过断点调试让大家明白为什 ConcurrentModificationException
是偶尔出现:
我在这三处地方都打了断点,这样我们就能大概清楚整个流程:
好的,我们看到已经定位到第一个断点位置了,从idea提供的信息我们也可以看出list的大小为2:
接着往下走,又来到了第二个断点的位置,在上面我已经说了 forEach
的语法的原理了,所以这样会走到 haxNext()
函数这里,这里的 cursor
是指当前迭代器的指针,而 size
是当前集合的大小:
继续走,我们会来到第三个断点:
注意我圈红的第一处地方,我们进入checkForComodification里:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } 复制代码
可以看到,这就是我们报错的关键点,这里的 modCount
变量是指集合被操作的次数,比如像 add()
、 remove()
这些方法都会让 modCount + 1
,而 expectedModCount
是指集合的一个预期操作次数,在部分操作里会被重置为 modCount
,比如 add()
方法里。
因为我们上面添加了两个元素,所以 modCount
和 modCount
都是2。
接着我们看第二处圈红的地方,我们可以发现,每一次 next()
的时候指针都会移动,这很好理解。
断点继续,因为第一个元素就是1,所以这里匹配上了:
我们进入到 remove()
方法里面,因为我们是按照对象删除的,所以会进入第二个分支:
接着我们再进入 fastRemove()
方法,可以看到 modCount + 1
了:
继续往下走,我们又回到最开始的地方,但仔细点你会发现list的大小从2变成1了:
然后我们又来到了 hasNext()
这里了,因为 cursor
和 size
都是1,所以循环就终止了:
这里你是不是懵逼了,咦?说好的报错呢?怎么没报错了?
咳咳,其实是因为有时候会出现像上面这种 巧合 的情况, 就是在 hasNext()
方法校验的时候, cursor
刚好不等于 size
,然后就退出了 ,而刚好集合又遍历完了,but,这个情况是很少出现的,一般都会抛出 ConcurrentModificationException
异常,所以大家不要有侥幸的心理。
下面我们还是以上面的例子,只是这次我把删除的对象 从1改为2 :
运行调试后跟上面的P1和P2是一样的,所以这里我就不重复了,唯一不同的地方在P3。这里我们已经来到第二次循环,校验元素后会删除元素2:
在第三次循环,(这里是指第三次进行 hasNext()
)的时候,我们可以看到list的大小是1了:
ok,我们继续往下,这里大家要特别注意, 可以看到 cursor
此时是2,而 size
却是1,所以循环还可以继续 :
前面我们说过 next()
方法里的 checkForComodification()
是检查操作次数的,所以这里就不复述了:
我们进入到 checkForComodification()
里, 可以看到 modCount
是3(因为 remove()
操作 **modCount**
自增了),而 expectedModCount
是2,所以就报错了 :