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

文章目录

  • 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.rhkb.cn/news/436825.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Nacos架构 原理】内核设计之Nacos通信通道

文章目录 Nacos通信通道 &#xff08;长链接&#xff09;现状背景场景分析配置服务 长链接核心诉求功能性诉求负载均衡连接生命周期 Nacos通信通道 &#xff08;长链接&#xff09; 现状背景 Nacos 1.X 版本 Config/Naming 模块各自的推送通道都是按照自己的设计模型来实现的…

仪器数码管数字识别系统源码分享

仪器数码管数字识别检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Comput…

Glide基本用法及With方法源码解析

文章目录 引入优点 使用步骤导入依赖权限使用 其他用法占位符错误图片后备回调符圆角过渡动画大小调整gif缩略图 使用RequestOptions缓存机制设置缓存策略清理缓存 使用集成库OkHttpVolley with源码解析getRetrieverGlide.getinitializeGlide getRequestManagerRetriever Reque…

【Spine】引入PhotoshopToSpine脚本

引入 右键Photoshop图标&#xff0c;选择属性 打开文件所在位置 找到目录下的\Presets\Scripts文件夹。 找到Spine目录下的\scripts\photoshop文件夹下的PhotoshopToSpine.jsx 复制它&#xff0c;丢到Photoshop刚才找的那个目录下。 使用 打开.psd文件&#xff0c;检查不要…

ubuntu 安装harbor

#安装包 wget https://github.com/goharbor/harbor/releases/download/v2.10.3/harbor-offline-installer-v2.10.3.tgz wget https://github.com/goharbor/harbor/releases/download/v2.10.3/harbor-offline-installer-v2.10.3.tgz.asc#导入签名公钥 gpg --keyserver hkps://ke…

文心一言 VS 讯飞星火 VS chatgpt (359)-- 算法导论24.3 1题

一、在图 24-2上运行Dijkstra算法&#xff0c;第一次使用结点 s s s作为源结点&#xff0c;第二次使用结点 z z z作为源结点。以类似于图 24-6 的风格&#xff0c;给出每次while循环后的 d d d值和 π π π值&#xff0c;以及集合 S S S中的所有结点。如果要写代码&#xff0c…

计算机毕业设计Spark+PyTorch股票预测系统 股票推荐系统 股票可视化 股票数据分析 量化交易系统 股票爬虫 股票K线图 大数据毕业设计 AI

《SparkPyTorch股票预测系统》开题报告 一、研究背景与意义 随着信息技术的飞速发展和全球金融市场的日益繁荣&#xff0c;股票投资已成为广大投资者的重要选择之一。然而&#xff0c;股票市场的复杂性和不确定性使得投资者在做出投资决策时面临巨大的挑战。传统的股票分析方…

Python空间地表联动贝叶斯地震风险计算模型

&#x1f3af;要点 使用贝叶斯推断模型兼顾路径和场地效应&#xff0c;量化传统地理统计曲线拟合技术。使用破裂和场地特征等地质信息以及事件间残差和事件内残差描述数学模型模型使用欧几里得距离度量、角距离度量和土壤差异性度量确定贝叶斯先验分布和后验分布参数&#xff…

CTMO时代下的营销新力量:2+1链动模式AI智能名片商城小程序

在当今这个瞬息万变的商业世界里&#xff0c;营销领域正经历着一场深刻的变革。传统的CMO岗位似乎在时代的浪潮中逐渐失去了它的光芒&#xff0c;CTMO正在悄然取代传统CMO的岗位。 随着营销丛林现象的出现&#xff0c;企业面临着前所未有的挑战。许多企业发现&#xff0c;那些传…

【SQL】未订购的客户

目录 语法 需求 示例 分析 代码 语法 SELECT columns FROM table1 LEFT JOIN table2 ON table1.common_field table2.common_field; LEFT JOIN&#xff08;或称为左外连接&#xff09;是SQL中的一种连接类型&#xff0c;它用于从两个或多个表中基于连接条件返回左表…

动态分配内存

目录 前言 一.malloc,free函数 1.malloc,free函数原型 2.使用方法 3.具体实例 4.注意事项 二.calloc函数 1.calloc函数原型 2.主要特点 3.使用案例 三.realloc函数 1.realloc函数原型 2.使用案例 3.注意事项 前言 动态内存是指在程序运行时&#xff0c;按需分配和…

51单片机学习第六课---B站UP主江协科技

DS18B20 1、基本知识讲解 2、DS18B20读取温度值 main.c #include<regx52.h> #include"delay.h" #include"LCD1602.h" #include"key.h" #include"DS18B20.h"float T; void main () {LCD_Init();LCD_ShowString(1,1,"temp…

时序必读论文14|VLDB24 TFB:全面且公平的时间序列预测方法框架

论文标题&#xff1a;TFB: Towards Comprehensive and Fair Benchmarking of Time Series Forecasting Methods 论文链接&#xff1a;https://arxiv.org/pdf/2403.20150.pdf 代码链接&#xff1a;https://github.com/decisionintelligence/TFB 前言 五一过后读的第一篇文章…

矩阵系统源码搭建的具体步骤,支持oem,源码搭建

一、前期准备 明确需求 确定矩阵系统的具体用途&#xff0c;例如是用于社交媒体管理、电商营销还是其他领域。梳理所需的功能模块&#xff0c;如多账号管理、内容发布、数据分析等。 技术选型 选择适合的编程语言&#xff0c;如 Python、Java、Node.js 等。确定数据库类型&…

计算机毕业设计 基于Python的广东旅游数据分析系统的设计与实现 Python+Django+Vue Python爬虫 附源码 讲解 文档

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

Qt 中的 QListWidget、QTreeWidget 和 QTableWidget:简化的数据展示控件

Qt 中的 QListWidget、QTreeWidget 和 QTableWidget&#xff1a;简化的数据展示控件 在 Qt 的用户界面开发中&#xff0c;展示和管理数据是常见的需求。Qt 提供了丰富的控件供开发者选择&#xff0c;其中 QListWidget、QTreeWidget 和 QTableWidget 是三个高层封装控件&#x…

vue实现文件解压缩

1. 使用CompressionStream API实现压缩 这里开启了多线程解压缩 <template><div class"page"><input type"file" placeholder"选择文件" id"file" /><button click"compress(compress)">压缩<…

uplink 级联口

Uplink 播报编辑讨论上传视频 交换机上的一种端口 展开2个同名词条 本词条缺少概述图&#xff0c;补充相关内容使词条更完整&#xff0c;还能快速升级&#xff0c;赶紧来编辑吧&#xff01; 在点到多点系统中&#xff0c;由分散点到集中点的传输链路。例如&#xff1a;在移动…

yolov8训练数据集——labelme的json文件转txt文件

yolov8的环境搭建&#xff0c;参考&#xff1a;Home - Ultralytics YOLO Docs 1.把标注好的json文件和jpg放同一个目录下。 2.运行转换脚本文件labelme2yolo.py文件&#xff1a; # -*- coding: utf-8 -*-import os import numpy as np import json from glob import glob im…

【C#】CacheManager:高效的 .NET 缓存管理库

在现代应用开发中&#xff0c;缓存是提升性能和降低数据库负载的重要技术手段。无论是 Web 应用、桌面应用还是移动应用&#xff0c;缓存都能够帮助减少重复的数据查询和处理&#xff0c;从而提高系统的响应速度。然而&#xff0c;管理缓存并不简单&#xff0c;尤其是当你需要处…