Java在用增强for循环遍历集合时删除元素,抛出java.util.ConcurrentModificationException异常

news2024/10/4 13:02:21

文章目录

  • 0. 前言
  • 1. 问题产生的背景
  • 2. Java中增强for循环的底层原理
  • 3. 为什么增强for循环不支持在遍历集合时删除元素
    • 3.1 问题排查
    • 3.2 modCount 变量的来源
    • 3.3 expectedModCount 变量的来源
    • 3.4 导致modCount变量和expectedModCount不相等的原因
    • 3.5 为什么用迭代器遍历元素时删除元素不会报错
    • 3.6 遍历 Map 等集合时删除元素会抛出 ConcurrentModificationException吗
  • 4. 如何正确地在遍历集合时删除元素
    • 4.1 使用迭代器进行删除(推荐使用)
    • 4.2 使用removeIf方法(推荐使用)
      • 4.2.1 实现了Collection接口的集合
      • 4.2.2 实现了Map接口的集合
    • 4.3 收集要删除的元素,遍历结束之后再删除
  • 5. 扩展:为什么用下标遍历元素集合时删除元素不会报错

0. 前言

本文讨论的是用增强 for 循环在遍历集合时删除元素的情况,用增强 for 循环在遍历集合时添加元素的情况类似

1. 问题产生的背景

在日常开发中,我们可能会遇到这样的需求,遍历一个集合,如果某些元素符合特定条件,我们就删除这些元素

我们以存储整数的 List 为例,如果是偶数,我们就从 List 中删除

在这里插入图片描述

import java.util.ArrayList;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        for (Integer i : arrayList) {
            if (i % 2 == 0) {
                arrayList.remove(i);
            }
        }

        arrayList.forEach(System.out::println);
    }

}

不出意外,运行代码后程序会抛出以下异常

在这里插入图片描述

Exception in thread “main” java.util.ConcurrentModificationException
at java.base/java.util.ArrayList I t r . c h e c k F o r C o m o d i f i c a t i o n ( A r r a y L i s t . j a v a : 1013 ) a t j a v a . b a s e / j a v a . u t i l . A r r a y L i s t Itr.checkForComodification(ArrayList.java:1013) at java.base/java.util.ArrayList Itr.checkForComodification(ArrayList.java:1013)atjava.base/java.util.ArrayListItr.next(ArrayList.java:967)
at cn.edu.scau.ListTests.main(ListTests.java:15)


为什么会抛出这个异常呢,我们一步一步分析

2. Java中增强for循环的底层原理

增强 for 循环的底层原理基于迭代器(Iterator),以下是增强 for 循环的工作原理:

  1. 目标集合:增强for循环可以用于任何实现了Iterable接口的对象,这意味着它可以直接用于大多数集合类型,如ListSetQueue(增强for循环通过自动装箱和自动拆箱来处理基本数据类型)
  2. 迭代器(Iterator):当增强for循环开始执行时,它会获取目标集合的迭代器。这个迭代器负责跟踪遍历的当前位置,并提供对集合中每个元素的访问
  3. 循环体:在每次迭代中,增强for循环调用迭代器的next()方法来获取下一个元素,并将其赋值给循环变量。然后执行循环体中的代码
  4. 结束条件:增强for循环会继续执行,直到迭代器的hasNext()方法返回false,这表示没有更多元素可以遍历

增强 for 循环的一些关键点:

  • 简洁性:增强 for 循环提供了更简洁的语法,不需要显式地创建迭代器
  • 限制:增强 for 循环不支持在遍历时修改集合(添加、删除元素),因为这样做可能会导致ConcurrentModificationException
  • 只读访问:增强for循环通常用于只读操作,如果要进行修改操作,通常需要使用传统的for循环或迭代器

总的来说,增强 for 循环是 Java 提供的一个语法糖,内部使用迭代器来实现遍历操作,使得代码更加简洁和易读

3. 为什么增强for循环不支持在遍历集合时删除元素

3.1 问题排查

我们以文章最开始的例子为例

在这里插入图片描述

要想知道为什么增强 for 循环不支持在遍历集合时删除元素,我们需要一步步地排查,中间可能会涉及到阅读源码的过程


我们点击错误信息中给出的 ArrayList.java:1013

在这里插入图片描述

可以发现,当 modCount 变量和 expectedModCount 不相等时,就会抛出 ConcurrentModificationException 异常

在这里插入图片描述

哪里调用了 checkForComodification 方法呢,我们点击错误信息中给出的 ArrayList.java:967

在这里插入图片描述

在这里插入图片描述

可以发现,在用迭代器进行遍历时,每一次调用 next 方法都会调用一次 checkForComodification 方法,检查 modCount 和 expectedModCount 是否相等

那 modCount 和 expectedModCount 是从哪里来的呢

3.2 modCount 变量的来源

我们同时按下 CTRL 和 鼠标左键,查看 modCount 变量的来源,点击之后跳转到了 AbstractList 类的源码,而 ArrayList 继承了 AbstractList 类,说明 modCount 变量继承自 AbstractList 类

在这里插入图片描述

3.3 expectedModCount 变量的来源

那 expectedModCount 又是从哪里来的呢,按下 CTRL 和 鼠标左键,查看 modCount 变量的来源

可以发现,expectedModCount 变量来自于 ArrayList 的一个私有内部类 Itr,Itr 类实现了 Iterator 接口,而且 expectedModCount 变量的初值就是 modCount

在这里插入图片描述

3.4 导致modCount变量和expectedModCount不相等的原因

为什么 modCount 变量和 expectedModCount 不相等呢

我们查看 ArrayList 类 remove 方法的源码

在这里插入图片描述

remove 方法里面又调用了 fastRemove 方法

在这里插入图片描述

可以看到,fastRemove 方法在删除元素时会让 modCount 变量自增 1 个单位,导致 modCount 变量和 expectedModCount 变量不相等

3.5 为什么用迭代器遍历元素时删除元素不会报错

我们改成用迭代器遍历,在遍历过程中删除元素

在这里插入图片描述

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            Integer i = iterator.next();
            if (i % 2 == 0) {
                iterator.remove();
            }
        }

        arrayList.forEach(System.out::println);
    }

}

运行程序,可以发现没有报错,为什么没有报错呢,增强 for 循环不也是基于迭代器的吗


为了弄清楚这个问题,我们需要查看一下ArrayList 中迭代器的源码

ArrayList 的迭代器是一个名为 Itr 的内部私有类,该内部私有类继承了顶层的 Iterator 接口

可以看到,每次获取迭代器返回的都是一个新的迭代器对象,新的迭代器对象的 expectedModCount 变量的值与 modCount 相等

在这里插入图片描述

我们继续查看迭代器提供的 remove 方法,按下 CTRL 和 鼠标左键,查看 remove 方法的源码

在这里插入图片描述

在这里插入图片描述

可以发现,remove 是 Iterator 接口中的默认方法,调用该方法会抛出异常

既然调用 ArrayList 迭代器的 remove 方法没有报错,说明 ArrayList 的迭代器必然重写了 remove 方法

我们查看 ArrayList 内部迭代器具体的 remove 方法,发现 ArrayList 内部的迭代器确实重写了 remove 方法,而且调用了 ArrayList 类的 remove 方法,而且调用完 ArrayList 类的 remove 方法之后,将 expectedModCount 变量重新赋值为 modCount,这也是为什么使用迭代器遍历元素时删除元素不会报错的原因

在这里插入图片描述

3.6 遍历 Map 等集合时删除元素会抛出 ConcurrentModificationException吗

上述的测试都是针对 List 集合来说的,那其它类型的集合呢

我们以 HashMap 为例,测试遍历 Map 时删除元素会不会抛出 ConcurrentModificationException 异常

在这里插入图片描述

import java.util.HashMap;
import java.util.Map;

public class MapDeleteTests {

    public static void main(String[] args) {
        Map<String, Object> hashMap = new HashMap<>();

        for (int i = 0; i < 10; i++) {
            hashMap.put("key" + i, i);
        }

        for (String key : hashMap.keySet()) {
            if (Integer.parseInt(key.substring(3)) % 2 == 0) {
                hashMap.remove(key);
            }
        }

        hashMap.forEach((k, v) -> System.out.println(k + ":" + v));
    }

}

不出意外,还是抛出 ConcurrentModificationException 异常,抛出异常的原因与 List 类似

HashMap 内部会维护 modCount 变量

在这里插入图片描述

而 expectedModCount 来自于 HashMap 内部的一个名为 HashIterator 的抽象类

在这里插入图片描述

HashIterator 类有三个具体的子类,分别对应 HashMap 的三种遍历方式

在这里插入图片描述

4. 如何正确地在遍历集合时删除元素

上面只是说到在用增强 for 循环遍历集合时删除元素会抛出异常的原因,那如果我真的要在遍历集合时删除元素(或者说添加元素),该怎么做呢

4.1 使用迭代器进行删除(推荐使用)

可以使用迭代器来遍历并删除元素,这样不会抛出异常

在这里插入图片描述

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            Integer i = iterator.next();
            if (i % 2 == 0) {
                iterator.remove();
            }
        }

        arrayList.forEach(System.out::println);
    }

}

4.2 使用removeIf方法(推荐使用)

removeIf 方法的底层也是使用了迭代器,需要配合 lambda 表达式使用

removeIf 方法的源码如下(removeIf 方法来自 Collection 接口)

在这里插入图片描述

对于不同的集合来说,removeIf 方法的使用方式不同,总体可分为两类:

  • 实现了 Collection 接口的集合
  • 实现了 Map 接口的集合

4.2.1 实现了Collection接口的集合

以 List 集合为例

在这里插入图片描述

import java.util.ArrayList;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        arrayList.removeIf(i -> i % 2 == 0);

        arrayList.forEach(System.out::println);
    }

}

4.2.2 实现了Map接口的集合

以 HashMap 为例

在这里插入图片描述

import java.util.HashMap;
import java.util.Map;

public class MapDeleteTests {

    public static void main(String[] args) {
        Map<String, Object> hashMap = new HashMap<>();

        for (int i = 0; i < 10; i++) {
            hashMap.put("key" + i, i);
        }

        hashMap.keySet().removeIf(i -> Integer.parseInt(i.substring(3)) % 2 == 0);

        hashMap.forEach((k, v) -> System.out.println(k + ":" + v));
    }

}

4.3 收集要删除的元素,遍历结束之后再删除

在这里插入图片描述

import java.util.ArrayList;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        ArrayList<Integer> elementToDeleteList = new ArrayList<>();

        for (int i = 0; i < arrayList.size(); i++) {
            if (i % 2 == 0) {
                elementToDeleteList.add(i);
            }
        }

        elementToDeleteList.forEach(arrayList::remove);

        arrayList.forEach(System.out::println);
    }

}

5. 扩展:为什么用下标遍历元素集合时删除元素不会报错

对于可以用下标遍历的集合来说,可以在用下标遍历集合元素的同时删除元素,因为使用下标遍历集合并没有使用到迭代器

但是不建议在用下标遍历元素集合时删除元素,因为在我们的编程习惯中,用于遍历集合的下标都是只读的,如果我们修改了下标,可能会造成一些意想不到的问题

唯一需要注意的是,删除元素后下标要减一,否则会跳过某个元素

在这里插入图片描述

import java.util.ArrayList;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        for (int i = 0; i < arrayList.size(); i++) {
            if (arrayList.get(i) % 2 == 0) {
                arrayList.remove(i);
                i--;
            }
        }

        arrayList.forEach(System.out::println);
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2188053.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学籍管理平台|在线学籍管理平台系统|基于Springboot+VUE的在线学籍管理平台系统设计与实现(源码+数据库+文档)

在线学籍管理平台系统 目录 基于SpringbootVUE的在线学籍管理平台系统设计与实现 一、前言 二、系统功能设计 三、系统实现 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大…

Leetcode: 0021-0030题速览

Leetcode: 0021-0030题速览 本文材料来自于LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer&#xff08;第 2 版&#xff09;》、《程序员面试金典&#xff08;第 6 版&#xff09;》题解 遵从开源协议为知识共享 版权归属-相同方式…

如此的“宠物医保”靠谱吗?

今天是世界动物日&#xff0c;本“人民体验官”推广人民日报官方微博文化产品《带着笑意的眼睛&#xff0c;能看见最美的风景》。 截图&#xff1a;来源“人民体验官”推广平台 人民微博说&#xff0c;带着笑意的眼睛&#xff0c;能看见最美的风景。生活中多一点微笑&#xff…

buuctf 部分misc题

rar 这个不多说了直接暴力破解&#xff0c;提示说的已经很清楚了&#xff0c;四位数密码&#xff1b; qr 这个就是一个二维码&#xff0c;用qr扫一下就出来了&#xff1b; 镜子里面的世界 用stegslove&#xff0c;打开dataextract进行调整 调整之后就可以得到flag了 ning…

ipv6之ospf配置

topo图 路由器均使用ipv6的地址 AR1使用默认路由到达ISP&#xff0c;在ospf上使用路由下发功能&#xff0c;把ISP的静态路由下发给内部网络 ISP使用7条静态路由&#xff0c;到达内部网络 AR1、AR2、AR3、AR4之间使用ospfv3进行通信 但是我配了下面的代码无法通信&#xff0…

driver,sequencer,sequence之间的握手关系_2024年10月3日

driver &#xff1a;根据接口协议将事务转换为一组信号级切换的组件。 sequencer &#xff1a;将事务&#xff08;sequence items&#xff09;从 sequence 发送到 driver&#xff0c;并将 driver 的响应反馈给 sequence 的组件。会对同时尝试访问 driver 以激励设计接口的多个 …

什么是 HTTP 请求中的 options 请求?

在 Chrome 开发者工具中的 Network 面板看到的 HTTP 方法 OPTIONS&#xff0c;其实是 HTTP 协议的一部分&#xff0c;用于客户端和服务器之间进行“预检”或“协商”。OPTIONS 请求的作用是让客户端能够获取关于服务器支持的 HTTP 方法和其他跨域资源共享 (CORS) 相关的信息&am…

spring揭秘25-springmvc04-servlet容器与springmvc容器总结

文章目录 【README】【1】DelegatingFilterProxy回顾【1.1】DelegatingFilterProxy初始化过滤器bean 【2】从servlet容器获取springmvc顶级web容器【2.1】从Servlet容器中获取springmvc容器总结【2.2】ContextLoaderListener加载springmvc顶级web容器并将其添加到servlet容器【…

基于SpringBoot+Vue的科研课题项目管理系统源码

文章目录 1.技术架构2.主要功能3.获取方式 1.技术架构 后端&#xff1a;SpringBoot 前端&#xff1a;Vue – Element UI 2.主要功能 登录 /注销、 用户管理、项目管理、申报管理、变更管理、 结题管理、角色管理、权限管理、数据字典等功能 3.获取方式 点击下方名片&a…

android 全面屏最底部栏沉浸式

Activity的onCreate方法中添加 this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); Android 系统 Bar 沉浸式完美兼容方案自 Android 5.0 版本&#xff0c;Android 带来了沉浸式系统 ba - 掘金 (juejin.cn)https://juejin.cn/post/7075578…

pycharm中使用anaconda创建多环境,无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称

问题描述 用的IDE是&#xff1a; 使用anaconda创建了一个Python 3.9的环境 结果使用pip命令的时候&#xff0c;报错 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称 解决方案 为了不再增加系统变量&#xff0c;我们直接将变量添加在当前项目中你的Ter…

Python3使用cv_bridge转换ROS的image信息

0. Preface 现在很多新的图片处理model都是基于python3的&#xff0c;而ROS还是2.7的&#xff0c;要结合起来用不可避免有很多问题&#xff0c;以下以接收ROS image为例子 以下方法需要用的anaconda&#xff0c;安装方法有很多blog分享 1. Preparation 以下是python3接收ima…

【黑马点评】0.环境配置--Redis6.2.6和可视化工具在Windows上的安装

黑马点评--0.Redis6.2.6在windows上的环境配置与可视化 0 前言1 下载安装2 解压后运行msi文件3 修改配置文件并打开Redis3.1 修改密码&#xff08;可选&#xff09;3.2 测试 4 Redis可视化&#xff08;可选&#xff09;4.1 Another Redis Desktop Manager下载安装4.2 连接Redis…

删除苹果手机所有照片的方法有哪些?

在这个数码摄影盛行的时代&#xff0c;手机相册里可能堆积了成千上万的照片&#xff0c;尤其是苹果手机用户。无论是为了释放存储空间&#xff0c;还是因为想要重新开始整理照片&#xff0c;删除所有照片可能成为一个必要的任务。那么&#xff0c;如何有效地删除删除苹果手机所…

防止老年痴呆的一题

如何只移动三个圆使下图变为正三角? 真相如此简单:

Redis常用命令(超详细整理)

Redis常用命令&#xff08;超详细整理&#xff09; 服务器相关命令 ping &#xff1a; 检测连接是否存活echo&#xff1a; 在命令行打印一些内容quit、exit&#xff1a; 退出客户端shutdown&#xff1a; 退出服务器端info&#xff1a; 返回redis相关信息config get dir/* 实时…

69.【C语言】动态内存管理(重点)(2)

目录 3.free函数 cplusplus网的翻译 提炼要点 使用 x86debug环境下, 打开内存窗口 建议 3.free函数 cplusplus的介绍 点我跳转 cplusplus网的翻译 函数 free void free (void* ptr); 释放内存块 之前通过调用malloc来分配一块内存,calloc和recalloc是来释放内存块的,让内…

Sublime快捷键的使用和修改

sublime快捷键 Ctrl Shift D 复制光标所在整行&#xff0c;插入到下一行Ctrl Shift K 删除整行 如果快捷键冲突了&#xff0c;就需要修改 sublime快捷键修改 示例&#xff1a;当前 Ctrl Shift D 冲突了 1.选择 首选项 -> 按键绑定-默认 2.按住 Ctrl F&#xff0…

初级前端面试常见问题(上岸某公司)

一、HTML5CSS篇 1.css隐藏元素方法 display:none position:absolute 移除到可视区域之外 visibility:hidden z-index:负值&#xff0c;用其他元素遮盖 opacity:0 clip/clip-…

心觉:做真正对自己成长有价值的事情

Hi&#xff0c;我是心觉&#xff0c;与你一起玩转潜意识、脑波音乐和吸引力法则&#xff0c;轻松掌控自己的人生&#xff01; 挑战每日一省写作189/1000天 不知不觉已经持续写了189天&#xff0c;每日一篇从不间断 这段时间最大的收获&#xff0c;就是看透了很多事物 自我觉…