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

最近在项目中发现有人用到了在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,这个底层加了道锁,并且每次删除新增都是去复制一份数组进行的,篇幅问题,下一次细说。

  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java,有多种方法可以将两个List集合进行拼接。 方法一是通过遍历集合的元素,并在遍历的过程完成添加操作。可以使用for-each循环遍历第一个List集合,将每个元素添加到新的List集合,然后再遍历第二个List集合,将每个元素也添加到新的List集合。最后,输出新的List集合即可。\[1\] 方法二是使用addAll()方法,该方法可以将一个集合的所有元素添加到另一个集合。可以先创建一个新的List集合,然后使用addAll()方法将第一个List集合和第二个List集合的元素都添加到新的List集合。最后,输出新的List集合即可。\[2\] 另外,如果需要拼接的字段是字符串类型,可以先将要拼接的字段全部拼接起来,不管值是否为空。可以使用split()方法将字符串拆分成数组,然后将数组转换为List集合。接着,可以使用Stream流对List集合进行过滤,去除值为"null"和空字符串的元素,得到最终的List集合。\[3\] 以上是几种常见的Java List集合拼接方法,你可以根据具体的需求选择适合的方法来实现拼接操作。 #### 引用[.reference_title] - *1* *2* [JAVA将两个列表(List)合并为一个列表](https://blog.csdn.net/JB666M/article/details/124467012)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [JAVA-List对象多个字段值拼接](https://blog.csdn.net/weixin_46690278/article/details/128232967)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值