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

原文地址:https://www.cnblogs.com/Xieyang-blog/p/9320943.html

时间: 2024-08-27 13:32:57

关于java中ArrayList的快速失败机制的漏洞——使用迭代器循环时删除倒数第二个元素不会报错的相关文章

ArrayList在foreach删除倒数第二个元素不抛并发修改异常的问题

平时我们使用ArrayList比较多,但是我们是否知道ArrayList在进行foreach的时候不能直接通过list的add或者move方法进行删除呢, 原因就是在我们进行foreach遍历的时候,其实底层原理就是使用了 iterator 迭代器进行操作的,我们在foreach中使用list的add 或者 move 方法:会导致并发修改异常抛出: ArrayList是java开发时非常常用的类,常碰到需要对ArrayList循环删除元素的情况.这时候大家都不会使用foreach循环的方式来遍历

java中ArrayList 、LinkList区别

转自:http://blog.csdn.net/wuchuanpingstone/article/details/6678653 个人建议:以下这篇文章,是从例子说明的方式,解释ArrayList.LinkedList,但是最好的方式还是看源代码.其实ArrayList就是一个动态数组,LinkedList是一个链表.  1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构.     2.对于随机访问get和set,ArrayList优于LinkedLis

JAVA学习篇--ThreadLocal,Java中特殊的线程绑定机制

在DRP项目中,我们使用了ThreadLocal来创建Connection连接,避免了一直以参数的形式将Connection向下传递(传递connection的目的是由于jdbc事务要求确保使用同一个connection连接).那么ThreadLocal是如果做到的呢?它和同步锁的不同在哪里? 是什么: 对于ThreadLocal看英文单词我们很容易理解为一个线程的本地实现,但是它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名为ThreadLoc

Java中arraylist和linkedlist源代码分析与性能比較

Java中arraylist和linkedlist源代码分析与性能比較 1,简单介绍 在java开发中比較经常使用的数据结构是arraylist和linkedlist,本文主要从源代码角度分析arraylist和linkedlist的性能. 2,arraylist源代码分析 Arraylist底层的数据结构是一个对象数组.有一个size的成员变量标记数组中元素的个数,例如以下图: * The array buffer into which the elements of the ArrayLis

JAVA中ArrayList用法

JAVA中ArrayList用法 2011-07-20 15:02:03|  分类: 计算机专业 |  标签:java  arraylist用法  |举报|字号 订阅 Java学习过程中做题时,用到ArrayList,在网上寻找到的学习资料.   摘自:     http://www.cnblogs.com/skylaugh/archive/2006/09/15/505346.html System.Collections.ArrayList类是一个特殊的数组.通过添加和删除元素,就可以动态改变

完成这个例子,说出java中针对异常的处理机制。

有一个类为ClassA,有一个类为ClassB,在ClassB中有一个方法b,此方法抛出异常,在ClassA类中有一个方法a,请在这个方法中调用b,然后抛出异常.在客户端有一个类为TestC,有一个方法为c ,请在这个方法中捕捉异常的信息.完成这个例子,请说出java中针对异常的处理机制. [java] view plaincopy package com.itheima; import java.io.IOException; /** *第6题:有一个类为ClassA,有一个类为ClassB,

Java中arraylist和linkedlist源码分析与性能比较

Java中arraylist和linkedlist源码分析与性能比较 1,简介 在java开发中比较常用的数据结构是arraylist和linkedlist,本文主要从源码角度分析arraylist和linkedlist的性能. 2,arraylist源码分析 Arraylist底层的数据结构是一个对象数组,有一个size的成员变量标记数组中元素的个数,如下图: * The array buffer into which the elements of the ArrayList are sto

Java集合(16)--快速失败机制(Fail-Fast)

迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证.快速失败迭代器会尽最大努力抛出 ConcurrentModificationException,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug. 它是Java集合的一种错误检测机制.当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制.记住是有可能,而不是一定. ConcurrentModificationExcep

Java中ArrayList源码分析

一.简介 ArrayList是一个数组队列,相当于动态数组.每个ArrayList实例都有自己的容量,该容量至少和所存储数据的个数一样大小,在每次添加数据时,它会使用ensureCapacity()保证容量能容纳所有数据. 1.1.ArrayList 的继承与实现接口 ArrayList继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口. public class  ArrayList<E> ex