当前位置: 首页 > article >正文

源码角度分析Java 循环中删除数据为什么会报异常

一、源码角度分析Java 循环中删除数据为什么会报异常

相信大家在之前或多或少都知道 Java 中在增强 for中删除数据会抛出:java.util.ConcurrentModificationException 异常,例如:如下所示程序:

public class RmTest {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("001");
        list.add("002");
        list.add("003");
        list.add("004");
        for (String l : list) {
            if (Objects.equals(l, "002") || Objects.equals(l,"003")) {
                list.remove(l);
            }
        }
        System.out.println(list);
    }
}

运行后会发现抛出了异常:

在这里插入图片描述

特别是一些新手小伙伴一不注意就陷入其中,当然解决方法也特别简单,可以转为迭代器,然后使用迭代器的 remove 方式删除数据,或者使用循环下标的方式通过下标进行删除,但需要注意正向循环和反向循环,如果是正向循环的话需要注意计算下标位置,不过不要担心,下面我们都会一一进行介绍。

首先来分析下为什么在增强 for 中会出现java.util.ConcurrentModificationException 异常,这里现将java编译成class形式,看增强 for最终是以何种形式执行的:

javac RmTest.java

编译后的内容如下:

public class RmTest {
    public RmTest() {
    }

    public static void main(String[] var0) {
        ArrayList var1 = new ArrayList();
        var1.add("001");
        var1.add("002");
        var1.add("003");
        var1.add("004");
        Iterator var2 = var1.iterator();

        while(true) {
            String var3;
            do {
                if (!var2.hasNext()) {
                    System.out.println(var1);
                    return;
                }

                var3 = (String)var2.next();
            } while(!Objects.equals(var3, "002") && !Objects.equals(var3, "003"));

            var1.remove(var3);
        }
    }
}

可以看到增强for最终是编译成迭代器的方式进行遍历数据,但需要注意的是删除数据依然使用的 List 中的 remove 方法,通过抛出的异常链可以看出,问题发生在了 next 方法中的 checkForComodification 方法下:

在这里插入图片描述

下面看到 ArrayList 下迭代器的 next 方法中,在 Itr 类下:

在这里插入图片描述
在这个方法中首先调用了 checkForComodification 方法,正好上面的异常链中也涉及到了 checkForComodification 方法,下面进到该方法中:

在这里插入图片描述
这里是不是看到了熟悉的 ConcurrentModificationException 异常,只要 modCountexpectedModCount 不相等就会抛出该异常,下面看下 expectedModCount 的声明位置:

在这里插入图片描述

在迭代器内部声明的,并且起始值等于 modCount,而 modCount 则在定义在 AbstractList 在迭代器的外部,这里还记得前面迭代器中使用的是 List 中的 remove 方法删除的数据,这里看到该方法中:

在这里插入图片描述
该方法实际的删除逻辑在 fastRemove 方法中,继续看到该方法下:

在这里插入图片描述
看到这里是不是很直观了,modCount 数值发生了变化,而迭代器中的expectedModCount 没有随之修改,就导致 expectedModCount != modCount 而抛出异常。

我们都知道使用迭代器中的 remove 方式是不会引发异常的,比如:

public class RmTest {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("001");
        list.add("002");
        list.add("003");
        list.add("004");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String l = iterator.next();
            if (Objects.equals(l, "002") || Objects.equals(l, "003")) {
                iterator.remove();
            }
        }
        System.out.println(list);
    }

}

运行结果:

在这里插入图片描述

为什么迭代器的 remove 可以呢,下面看到该方法中:

在这里插入图片描述

可以看出迭代器的 remove 同样也是使用了 List 中的 remove 方法,但它会在删除后重置 expectedModCount 的值,使其保持和 modCount 一致,因此就不会触发上面的异常。

看到这里应该明白为什么会抛出异常了,但为什么这样设计呢?这里可以总结下其中,modCount主要表示集合被修改的次数,expectedModCount表示迭代器内部维护的集合被修改的次数。当modCountexpectedModCount不相等时,则表示肯定有其他某个地方对集合进行了修改,此时,如果继续使用迭代器遍历集合,就可能会出现遍历到非预期的元素或者下个元素不存在了,因此只要expectedModCountmodCount保持一致,数据就可认为是可信的。

通过这里也能给我们警醒,如果需要在并发情况下操作集合一定要选用线程安全的集合。

下面再补充下如果不用增强for,使用下标自增的方式删除是否可行吗?

public class RmTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("001");
        list.add("002");
        list.add("003");
        list.add("004");
        for (int i = 0; i < list.size(); i++) {
            String l = list.get(i);
            if (Objects.equals(l, "002") || Objects.equals(l,"003")) {
                list.remove(i);
            }
        }
        System.out.println(list);
    }
}

运行后:

在这里插入图片描述

发现 003 并没有被移除,因为当移除了 002 后,002 后的数据顺势向前移位,原本003的下标为 2 ,移位后变成了 1 ,但下标 i 继续增长,便会错过后面的数据,那怎么解决呢,既然后面的数据向前移位,对下标i也向前移位就是了:

public class RmTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("001");
        list.add("002");
        list.add("003");
        list.add("004");
        for (int i = 0; i < list.size(); i++) {
            String l = list.get(i);
            if (Objects.equals(l, "002") || Objects.equals(l,"003")) {
                list.remove(i);
                i = i-1;
            }
        }
        System.out.println(list);
    }
}

运行后数据正常:

在这里插入图片描述

既然正向遍历下标需要移位,那如果反过来反向循环不就可以不管下标了吗:

public class RmTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("001");
        list.add("002");
        list.add("003");
        list.add("004");
        for (int i = list.size() - 1; i >= 0; i--) {
            String l = list.get(i);
            if (Objects.equals(l, "002") || Objects.equals(l, "003")) {
                list.remove(i);
            }
        }
        System.out.println(list);
    }
}

运行后数据正常:

在这里插入图片描述


http://www.kler.cn/a/107519.html

相关文章:

  • 学习记录:js算法(九十二):克隆图
  • Bugku CTF_Web——文件上传
  • 2024/11/13 英语每日一段
  • 算法——移除链表元素(leetcode203)
  • 鸿蒙next版开发:相机开发-元数据(ArkTS)
  • 基于迭代重加权最小二乘法的算法及例程
  • PHP与mysql数据库交互
  • 常用字符串函数拓展
  • 在本地模拟C/S,Socket套接字的使用
  • 【原创】解决Kotlin无法使用@Slf4j注解的问题
  • 设计模式(13)适配器模式
  • GZ035 5G组网与运维赛题第4套
  • 【Android】一个contentResolver引起的内存泄漏问题分析
  • 通信基础(一):数据传输基础
  • Java工具库——Commons IO的50个常用方法
  • 软考系统架构之案例篇(软件工程相关概念)
  • .net 7 上传文件踩坑
  • 【Python机器学习】零基础掌握DecisionBoundaryDisplay检验、检查
  • SpringMVC Day 05 : Spring 中的 Model
  • react实现步进器
  • flutter升级+生成drift文件
  • hadoop使用简介
  • C# 递归算法使用简介_常用整理
  • Vue引入异步组件
  • STM32F4X SDIO(二) SDIO协议
  • 使用Hystrix实现请求合并,降低服务器并发压力