关于java中ArrayList的快速失败机制的漏洞——使用迭代器循环时删除倒数第二个元素不会报错
Posted xieyang-blog
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于java中ArrayList的快速失败机制的漏洞——使用迭代器循环时删除倒数第二个元素不会报错相关的知识,希望对你有一定的参考价值。
一、问题描述
话不多说,先上代码:
public static void main(String[] args) throws InterruptedException { List<String> list = new ArrayList<String>();
list.add("第零个"); list.add("第一个"); list.add("第二个"); list.add("第三个"); list.add("第四个"); for (String str : list) { if (str.equals("第三个")) { System.out.println("删除:" + str); list.remove(str); } } System.out.println(list); }
知道快速失败机制的可能都会说,不能在foreach循环里用集合直接删除,应该使用iterator的remove()方法,否则会报错:java.util.ConcurrentModificationException
但是这个代码的真实输出结果却是:
并没有报错,这是为什么呢?
二、基础知识
java的foreach 和 快速失败机制还是先简单介绍一下:
foreach过程:
Java在通过foreach遍历集合列表时,会先为列表创建对应的迭代器,并通过调用迭代器的hasNext()函数判断是否含下一个元素,若有则调用iterator.next()获取继续遍历,没有则结束遍历。
快速失败机制:
因为非线程安全,迭代器的next()方法调用时会判断modCount==expectedModCount,否则抛出ConcurrentModIficationException。modCount只要元素数量变化(add,remove)就+1,而集合和表的add和remove方法却不会更新expectedModCount,只有迭代器remove会重置expectedModCount=modCount,并将cursor往前一位。所以在使用迭代器循环的时候,只能使用迭代器的修改。
三、分析
所以由上面的foreach介绍我们可以知道上面的foreach循环代码可以写成如下形式:
for (Iterator iterator = list.iterator(); iterator.hasNext();) { String str = (String) iterator.next(); if (str.equals("第三个")) { System.out.println("删除:" + str); list.remove(str); } }
重点就在于 iterator.next() 这里,我们看看next()的源码:【此处的迭代器是ArrayList的私有类,实现了迭代器接口Iterator,重写了各种方法】
1 public E next() { 2 checkForComodification(); 3 try { 4 int i = cursor; 5 E next = get(i); 6 lastRet = i; 7 cursor = i + 1; 8 return next; 9 } catch (IndexOutOfBoundsException e) { 10 checkForComodification(); 11 throw new NoSuchElementException(); 12 } 13 }
注意到第7行!,也就是说,cursor最开始是 i,调用next()后就将第 i 个元素返回,然后变成下一个元素的下标了,所以在遍历到倒数第二个元素的时候cursor已经为最后一个元素的下标(size-1)了,
注意了!在调用集合或者.remove(int)的方法时,并不会对cursor进行改变,【具体操作:将元素删除后,调用System.arraycopy让后面的的元素往前移动一位,并将最后一个元素位释放】,而本程序中此时的size变成了原来的size-1=4,而cursor还是原来的size-1=4,二者相等!,再看看判定hasNext():
public boolean hasNext() { return cursor != size(); }
此时cursor==size()==4,程序以为此时已经遍历完毕,所以根本不会进入循环中,也就是说根本不会进入到next()方法里,也就不会有checkForComodification() 的判断。
四、验证
我们在程序中foreach循环的第一句获取str后面加入一个打印, System.out.println(str); ,
这样每次进入foreach循环就会打印1,其他不变,我们再来运行一次,结果如下:
显然,第四个元素没有被遍历到,分析正确,那假如使用iterator.remove()呢?
那我们再来看看iterator.remove()的源码,【此处的iterator是ArrayList的私有类,实现了迭代器接口Iterator,重写了各种方法】
1 public void remove() { 2 if (lastRet < 0) 3 throw new IllegalStateException(); 4 checkForComodification(); 5 6 try { 7 AbstractList.this.remove(lastRet); 8 if (lastRet < cursor) 9 cursor--; 10 lastRet = -1; 11 expectedModCount = modCount; 12 } catch (IndexOutOfBoundsException e) { 13 throw new ConcurrentModificationException(); 14 } 15 }
看第7行,内部其实也是调用的所属list的remove(int)方法,但是不同的地方要注意了:
第9行:将cursor--,也就是说在删除当前元素后,他又把移动后的指针放回了当前元素下标,所以继续循环不会跳过当前元素位的新值;
第11行:expectedModCount = modCount; 更新expectedModCount,使二者相同,在继续循环中不会被checkForComodification()检测出报错。
五、结论
1. 每次foreach循环开始时next()方法会使cursor变为下一个元素下标;
2. ArrayList的remove()方法执行完后,下一个元素移动到被删除元素位置上,由1可知,cursor此时指向原来被删除元素的下一个的下一个元素所在位置,此时继续foreach循环,被删除元素的下一个元素不会被遍历到;
3. checkForComodification()方法用来实现快速失败机制的判断,此方法在iterator.next()方法中,必须在进入foreach循环后才会被调用;
4. 由2,当ArrayList的remove()方法在foreach删除倒数第二个元素时,继续foreach循环,倒数第一个元素会被跳过,从而退出循环,联合3可知,在删除倒数第二个元素后,并不会进入快速失败机制的判断。
5. iterator.remove()方法会在删除和移动元素后将cursor放回正确的位置,不会导致元素跳过,并且更新expectedModCount,是一个安全的选择。
以上是关于关于java中ArrayList的快速失败机制的漏洞——使用迭代器循环时删除倒数第二个元素不会报错的主要内容,如果未能解决你的问题,请参考以下文章