阿里巴巴java开发手册的建议
在看阿里巴巴Java开发手册时,有一条建议是这样的。
【强制】不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。
反例:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
说明:这个例子的执行结果会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?
正例:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
以上就是手册的建议内容。
我的测试
试验结果表明,为“1”时没有任何错误,“2”时,出现ConcurrentModificationException,也就是并发修改异常。
异常信息如下
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
为了看清foreach的真面目,这里将class文件反编译,就明白运行过程了
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
foreach遍历集合,其实是走的Iterator,首先判断hasNext(),如果没有了则终止循环,否则next()获取元素时,next()时,都要check一下集合元素个数是否变化了,如果变化了,则抛出异常。
查看源代码,Itr是ArrayList的内部类,实现了Iterator接口
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
modCount是集合添加元素、删除元素的次数,expectedModCount是预期的修改次数。因为我们增加了2个元素,所以,modCount是2,预期修改的次数expectedModCount也是2
现在解释一下,为什么“1”时没有出错,2是却出错了。
“1”时,第一次循环后,光标变为1,第一个元素已经移除,所以元素个数是1;进入第二次循环,判断hasNext()由于光标=元素个数,所以,终止循环,所以,就结束了。
“2”时,第二次循环后,光标变为2,此时元素个数是1,光标!=元素个数,所以进入next()方法,next()中调用checkForComodification,modCount是3,expectedModCount是2,两者不相等,所以抛出异常。
遍历集合删除元素的正确方式
迭代器方式移除
那么如果我们既想遍历元素又想增加/删除元素怎么办?
第一个方法,可以使用迭代器的remove方法,而不是集合的remove方法。这是因为,迭代器的remove方法会修改expectedModCount,从而使modCount与之相等
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
迭代器操作元素样例,这种不会出现并发修改异常。
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
注意,next()方法,不只返回元素,也改变了光标位置。所以如果执行两次next()就会跳过两个元素。
for(int i=0; i<list.size; i++)正序方式
有同学会问,如果使用
- 1
- 2
- 3
- 1
- 2
- 3
会如何呢?使用这种方法,如果删除元素,那么会跳过某些元素。可以重置index的方式来修正。比如
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
关于这一点,这里解释一下。比如,咱们要遍历一个集合,如果等于1就移除掉,否则全部加1,代码如下
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
运行结果
- 1
- 1
结果出人意料吧?为什么2没有加1?!简单来说,移除元素后,后边的元素会往前移动,将空白填充上。
第一次循环,i=0,移除了第一个元素后,i变为1,但是,同时,由于1的空出,2、3都往前移动一个位置,所以,索引位置不再是1、2,而是0、1
第二次循环,i=1,3的索引是1,所以只将3加1,而2已经过去了。
for(int i=list.size-1; i>=0; i - -)倒序方式
由正序方式的缺点为出发点思考,如果倒序的遍历,中间即使有删除也不会漏掉元素。这里就不再举例子了。