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,是一个安全的选择。

  • 相关阅读:
    Vue 2.x windows环境下安装
    VSCODE官网下载缓慢或下载失败 解决办法
    angular cli 降级
    Win10 VS2019 设置 以管理员身份运行
    XSHELL 连接 阿里云ECS实例
    Chrome浏览器跨域设置
    DBeaver 执行 mysql 多条语句报错
    DBeaver 连接MySql 8.0 报错 Public Key Retrieval is not allowed
    DBeaver 连接MySql 8.0报错 Unable to load authentication plugin 'caching_sha2_password'
    Linux系统分区
  • 原文地址:https://www.cnblogs.com/Xieyang-blog/p/9320943.html
Copyright © 2011-2022 走看看