list的for循环中调用增删操作所带来的的思考

文章探讨了在Java中使用for循环、迭代器和增强for循环进行列表元素添加删除时遇到的ConcurrentModificationException异常,解释了迭代器删除为何不会报错,以及ArrayList底层modCount和expectedModCount机制的作用。
摘要由CSDN通过智能技术生成

最近在项目中发现有人用到了在for循环中去进行列表的添加删除,最后报错,我就试着去研究底层代码。

发现问题

首先我们来看这段代码

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        //迭代器
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            String next = iterator.next();
            if(next.contains("1")){
                list.remove(next);
            }
        }
    }

我们发现这次会报这个错误

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.zsap.project.server.bootstrap.controller.PrjProjTeamController.main(PrjProjTeamController.java:87)

那我们如果用for循环呢

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        for (String s : list) {
            if(s.contains("1")){
                list.remove(s);
            }
        }
 }

同样的报错

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.zsap.project.server.bootstrap.controller.PrjProjTeamController.main(PrjProjTeamController.java:84)

那如果是普通for循环呢

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        for (int i = 0; i < list.size(); i++) {
            if(list.get(i).contains("1")){
                list.remove(i);
            }
        }
 }

运行正常


Process finished with exit code 0

那如果是迭代器删除呢

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        //迭代器
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            String next = iterator.next();
            if(next.contains("1")){
                iterator.remove();
            }
        }
    }

同样没有报错


Process finished with exit code 0

接下来我们从底层代码分析

底层分析
  1. 我们直接点进报错信息查看为什么报错(分析第一种情况迭代循环)

在这里插入图片描述

我们可以看到是modCount和count不一致所导致的异常发生。
接下来进行打断点发现是在**iterator.next()**所导致的报错

  1. 我们得确认这个是在那个具体的实现方法所报的错

在这里插入图片描述
使用对象.getClass().getName()进行查找

 public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        //迭代器
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            String next = iterator.next();
            System.out.println(iterator.getClass().getName());
            if(next.contains("1")){
                list.remove(next);
            }
        }
    }

查看打印

java.util.ArrayList$Itr
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)

我们这里发现是ArrayList类下的
在这里插入图片描述
逐步分析

public E next() {
             //进行我们上一个分析出来报错的方法,也就是校验modcount和expectedModCount是否相等
            checkForComodification();
            //游标
            int i = cursor;
            //如果现在的游标已经大于数组大小报异常(ArrayList底层是动态数组,观察源码就能知道)
            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];
        }

根据打断点我们发现是checkForComodification();方法报错

  1. 接下来我们要去找到为什么modcount和expectedModCount会不一致
    我们查看源码发现modcount初始值为0

    在这里插入图片描述
    而expectedModCount初始化为modCount的值
    在这里插入图片描述
    我们查看list.remove()源码
public boolean remove(Object o) {
//如果传进来的对象是空的
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
        //遍历数组
            for (int index = 0; index < size; index++)
            //发现数组上的对应元素和传进来的对象相等
                if (o.equals(elementData[index])) {
                //删除操作
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

可以发现关键代码是fastRemove(index);
继续往下

    private void fastRemove(int index) {
    //modCount++
        modCount++;
        //数组内排在被移除元素之后的元素个数
        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
    }

在这段代码我们可以看到modCount是在这里发生了变化。

  1. 我们打断点之前发现modCount已经是4了,expectedModCount为3,说明在remove之前已经进行了modCount++,这时我们观察list的add()方法
    public boolean add(E e) {
        //我们看到jdk官方已经在这里注释了 增加modCount!!
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

继续往下

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

答案就在ensureExplicitCapacity方法

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

至此我们就能发现报错的原因,modCount相当于变化的次数,我们先往列表添加了三条,此时modCount为3,我们创建构造器,此时expectedModCount被modCount赋值初始化为3,再进行remove()方法,此时modCount为4,最后在next方法进行第一行校验时抛出异常。

  1. 那么为什么迭代器删除就不会报错呢?
    其实观察迭代器的remove方法就能知道了
    在这里插入图片描述
    我们其实可以看到remove方法每次都保证了这两个值的一致性,也就不会报错啦!

总结

我们最后总结一下,其实就是迭代器并不知道你进行了删除操作,所以我期望的改变值不等于改变值,java认为此时是不安全的,就会快速失败。
而用迭代器自己的方法,就会保证迭代器知道我们每次改变的次数。
我们可以去研究普通for循环删除为什么不会报错发现在普通for循环中无论根据下标删除或者对象直接删除,都会去更新modCount记录改变次数,但并没有比较期望次数(这是迭代器独有的),所以不会去抛出异常,正常删除。
至于为什么增强for也会抛出异常,其实增强for是迭代器循环的语法糖,本质还是迭代器循环。

拓展

那有什么list可以在循环中删除保证安全呢,答案是copyOnWriteList,这个底层加了道锁,并且每次删除新增都是去复制一份数组进行的,篇幅问题,下一次细说。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值