zoukankan      html  css  js  c++  java
  • 关于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,是一个安全的选择。

  • 相关阅读:
    Ubuntu JDK 安装及环境配置
    流式计算之Storm简介
    Amcharts 提示 字体找不到解决方法
    Amcharts 入门教程
    JavaMail 发送邮件简单 Demo
    JavaMail 发送邮件,以及sina、163、QQ服务器不同的解析结果(附图)
    MyEclipse 搭建 hadoop 环境
    淘宝主搜索体验
    【转】Net中VSS实现版本控制管理的一些使用方法
    IIS配置PHP环境(快速最新版)
  • 原文地址:https://www.cnblogs.com/Xieyang-blog/p/9320943.html
Copyright © 2011-2022 走看看