分析ArrayList在遍历时修改报错的原因

Posted LiuJun2Son

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分析ArrayList在遍历时修改报错的原因相关的知识,希望对你有一定的参考价值。

以前使用for(:)遍历List集合并同时修改List集合中的内容时会报:ConcurrentModificationException错误,这个错误就是提示我们:方法中有对象的并发修改,但不允许这种修改时,所以抛出此异常。

1.模拟遍历集合的同时修改集合(一)

新建一个list集合,并给该集合依次添加1-5的字符窜,然后遍历集合,在遍历集合的时候删除”2”

        public static void main(String[] args) 
             List<String> list = new ArrayList<String>();
                list.add("1");
                list.add("2");
                list.add("3");
                list.add("4");
                list.add("5");
                System.out.println("原来的list:" + list);
                for (String string : list) 
                    System.out.println(string);
                    //如果获取的内容是"2",就把它删除
                    if ("2".equals(string)) 
                        list.remove(string);
                    
                
                System.out.println("修改后的list:: " + list);
        

输出的结果:

    原来的list:[1, 2, 3, 4, 5]
    1
    2
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
        at java.util.ArrayList$Itr.next(Unknown Source)
        at com.test.jh.TestListDemo.main(TestListDemo.java:16)

当集合遍历到”2”这个元素的时候,就出现了ConcurrentModificationException异常,拿到底是哪一个方法抛出的异常?其实是Iterator类中next()的方法抛出的异常。

2.foreach遍历集合的原理分析

通过反编译技术,反编译上面mian方法中的代码

    public static void main(String args[])
    
        List list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        //1.字符串通过+拼接的底层实现也是通过StringBuilder这个类
        System.out.println((new StringBuilder("原来的list:")).append(list).toString());
        //2.使用for (String string : list) 遍历集合的底层是使用Iterators实现
        for (Iterator iterator = list.iterator(); iterator.hasNext();)
        
            //3.重点:这里调用了next()方法,其实上面的那个错误就是调用了这个方法报错的
            String string = (String)iterator.next();
            System.out.println(string);
            if ("2".equals(string))
                list.remove(string);
        

        System.out.println((new StringBuilder("修改后的list:: ")).append(list).toString());
    

同过分析foreach遍历集合的原理,遍历集合的底层是使用Iterators实现,并且报错是因为执行了String string = (String)iterator.next();这一行代码

3.ArrayList的源码分析

查看Iterator中的next()方法

因为报错的原因是执行了:String string = (String)iterator.next();这句,那查看Iterator类的代码

        //把集合修改的次数(modCount)赋值给expectedModCount记录下来
        int expectedModCount = modCount;    

        public E next() 
            //主要用来判断集合的修改次数是否合法
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        


        //主要用来判断集合的修改次数是否合法
        final void checkForComodification() 
            //modCount代表集合修改的次数(例如:每list.add()一次就加1,list.remove()一次也加1)
            //expectedModCount的值是等于开始遍历集合时的修改的次数
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        

checkForComodification()方法中的modCount与expectedModCount的值不相等就会抛出异常。那么他们两个值分别在哪里被修改了呢?其实modCount的值是在list集合执行add,remove等操作的时候赋值,而expectedModCount是在new Iterator()时,把modCount的值赋给了expectedModCount记录。

查看ArrayList类的源码

那查看ArrayList类的remove()代码,发现remove方法中真的有对modCount++

    public E remove(int index) 
        rangeCheck(index);
        //记录了集合修改的次数,其实集合中的add()方法中也有modCount++;的代码
        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    

聪明的你,从上面的代码就可以知道,当我们遍历集合的并修改集合的内容时,为什么会导致checkForComodification()中的值不相等就会抛出ConcurrentModificationException异常。

4.遍历List集合并同时修改List集合中的内容报错的原因

    public static void main(String args[])
    
        List list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");

        System.out.println((new StringBuilder("原来的list:")).append(list).toString());
        //3.重点:list.iterator()
        for (Iterator iterator = list.iterator(); iterator.hasNext();)
        
            String string = (String)iterator.next();
            System.out.println(string);
            if ("2".equals(string))
                list.remove(string);
        

        System.out.println((new StringBuilder("修改后的list:: ")).append(list).toString());
    

1.当代码在遍历的时候执行到list.iterator()时候就创建了一个Iterator对象,并在创建这个Iterator对象的时候modCount的值赋值给了expectedModCount,这时expectedModCount的值就是集合当前修改的次数。

     //执行list.iterator()调用了这个方法
     public Iterator<E> iterator() 
        return new Itr();
     

     private class Itr implements Iterator<E> 
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        //创建这个Iterator对象的时候modCount的值赋值给了expectedModCount,这时expectedModCount的值就是集合当前修改的次数
        int expectedModCount = modCount;

        public boolean hasNext() 
            return cursor != size;
        

        .....
    

2.当代码在执行到下面的这个语句的时候,执行了list.remove(),这个时候modCount++;导致了modCount与expectedModCount不相等。那些下一次的循环在调用String string = (String)iterator.next();的时候就会导致next()方法中的checkForComodification()方法中的modCount与expectedModCount的值不相等就会抛出异常ConcurrentModificationException。

    if ("2".equals(string))
                list.remove(string);

  • 总结

    其实出现异常就是我们在遍历集合的时候修改了集合的内容,导致modCount的值在增加,而expectedModCount得值没有增加,所以在调用next()方法中的checkForComodification()方法判断集合的修改次数是否合法就会报错。

6.模拟遍历集合的同时修改集合(二)

新建一个list集合,并给该集合依次添加1-5的字符窜,然后遍历集合,在遍历集合的时候删除”4”(倒数第二个)

        public static void main(String[] args) 
             List<String> list = new ArrayList<String>();
                list.add("1");
                list.add("2");
                list.add("3");
                list.add("4");
                list.add("5");
                System.out.println("原来的list:" + list);
                for (String string : list) 
                    System.out.println(string);
                    //重点:是删除"4",而不是"2",那么会报错吗?其实不会
                    if ("4".equals(string)) 
                        list.remove(string);
                    
                
                System.out.println("修改后的list:: " + list);
        

输出的结果是:

    原来的list:[1, 2, 3, 4, 5]
    1
    2
    3
    4
    修改后的list:: [1, 2, 3, 5]

从上面的可以看出,靠,居然没有报错成功删除了?这是什么情况?

分析原因

1.当执行到下面的代码的时候,list.remove()删除了一个元素,导致size也减少了1。

    //当前的cursor=4,size=5
    if ("4".equals(string)) 
         list.remove(string);//删除了一个size-1等于4
    

2.接着当增强for循环再次判断hasNext():

    public boolean hasNext() 
            //当遍历到"4"的时候,当前的cursor=4,当前的size=4
            return cursor != size;
    

3.当执行到for (Iterator iterator = list.iterator(); iterator.hasNext();)的iterator.hasNext()时,返回的值是false。为什么呢?当遍历到”4”的时候,当前的cursor=4,size=4,导致hasNext()返回的值是false导致直接退出了for循环,漏掉了第五次循环(第五次也是最后一个循环),导致没有执行String string = (String)iterator.next();就没有报错。

4.如果我们删除的那个不是”4”,而是”3”,结果导致当前的cursor=3,当前的size=4,当下一次执行iterator.hasNext()判断时,返回的结果是true,导致String string = (String)iterator.next();会执行,删除一个元素后,导致modCount的值在增加,而expectedModCount得值没有增加,所以在调用next()方法中的checkForComodification()方法判断集合的修改次数是否合法就会报错。

以上是关于分析ArrayList在遍历时修改报错的原因的主要内容,如果未能解决你的问题,请参考以下文章

debug运行可以,release运行报错的原因及修改方法

linux系统中修改密码报错的问题

ArrayList 使用 forEach 遍历时删除元素会报错吗?

maven项目打包分析及打包后war包缺少配置文件报错的原因分析,使用progard混淆时配置分析

[Go]新手入门:map的介绍与使用

[Go]新手入门:map的介绍与使用