为什么不能在foreach中删除元素

news2024/11/26 3:24:04

文章目录

  • 快速失败机制(fail-fast)
  • for-each删除元素为什么报错
    • 原因分析
    • 逻辑分析
  • 如何正确的删除元素
    • remove 后 break
    • for 循环
    • 使用 Iterator
  • 总结

快速失败机制(fail-fast)

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system’s state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.

这是快速失败机制的英文解释。翻译过来就是:系统设计中,“fail-fast”指的是一种策略,系统或模块被设计成在出现错误或失败时立即检测并报告。这种方法旨在通过停止正常操作而不是继续可能存在缺陷的过程来最小化失败的影响。fail-fast系统通常在操作的多个点检查系统状态,以便及早发现任何失败。fail-fast模块的责任是检测错误,然后让系统的更高级别处理它们。

这段话的大致意思就是,fail-fast 是一种通用的系统设计思想,一旦检测到可能会发生错误,就立马抛出异常,程序将不再往下执行

很多时候,我们会把 fail-fast 归类为 Java 集合框架的一种错误检测机制,但其实 fail-fast 并不是 Java 集合框架特有的机制

for-each删除元素为什么报错

下面这段代码:

List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");

        for (String str : list) {
            if ("1".equals(str)) {
                list.remove(str);
            }
        }

        System.out.println(list);

在执行完之后就会报错
在这里插入图片描述
看一下报错的原因是在checkForComodification这里报的错。下面是具体的代码

final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

也就是说,remove 的时候触发执行了 checkForComodification 方法,该方法对 modCount 和 expectedModCount 进行了比较,发现两者不等,就抛出了 ConcurrentModificationException 异常。

原因分析

为什么会执行checkForComodification 方法呢?是因为for-each的底层是迭代器Iterator配合while来实现的

List<String> list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
Iterator var2 = list.iterator();

while(var2.hasNext()) {
    String str = (String)var2.next();
    if ("1".equals(str)) {
        list.remove(str);
    }
}

System.out.println(list);

看一下list的迭代器,点进iterator这个方法,发现它实现了Iterator接口

在这里插入图片描述
再去看一下 Itr 这个类。

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        // prevent creating a synthetic constructor
        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            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];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        public void forEachRemaining(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i < size) {
                final Object[] es = elementData;
                if (i >= es.length)
                    throw new ConcurrentModificationException();
                for (; i < size && modCount == expectedModCount; i++)
                    action.accept(elementAt(es, i));
                // update once at end to reduce heap write traffic
                cursor = i;
                lastRet = i - 1;
                checkForComodification();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

也就是说 new Itr() 的时候 expectedModCount 被赋值为 modCount,而 modCount 是 ArrayList 中的一个计数器,用于记录 ArrayList 对象被修改的次数。ArrayList 的修改操作包括添加、删除、设置元素值等。每次对 ArrayList 进行修改操作时,modCount 的值会自增 1。

在迭代 ArrayList 时,如果迭代过程中发现 modCount 的值与迭代器的 expectedModCount 不一致,则说明 ArrayList 已被修改过,此时会抛出 ConcurrentModificationException 异常。这种机制可以保证迭代器在遍历 ArrayList 时,不会遗漏或重复元素,同时也可以在多线程环境下检测到并发修改问题。

逻辑分析

List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");

        for (String str : list) {
            if ("1".equals(str)) {
                list.remove(str);
            }
        }

        System.out.println(list);

由于 list 此前执行了 3 次 add 方法。

  • add 方法调用 ensureCapacityInternal 方法
  • ensureCapacityInternal 方法调用ensureExplicitCapacity 方法
  • ensureExplicitCapacity 方法中会执行 modCount++

所以 modCount 的值在经过三次 add 后为 3,于是 new Itr() 后 expectedModCount 的值也为 3(回到前面去看一下 Itr 的源码)。

接着来执行 for-each 的循环遍历。

执行第一次循环时,发现“沉默王二”等于 str,于是执行 list.remove(str)。

  • remove 方法调用 fastRemove 方法
  • fastRemove 方法中会执行 modCount++

modCount 的值变成了 4。

第二次遍历时,会执行 Itr 的 next 方法(String str = (String) var3.next();),next 方法就会调用 checkForComodification 方法。

此时 expectedModCount 为 3,modCount 为 4,就只好抛出 ConcurrentModificationException 异常了。

如何正确的删除元素

remove 后 break

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");

for (String str : list) {
	if (1".equals(str)) {
		list.remove(str);
		break;
	}
}

break 后循环就不再遍历了,意味着 Iterator 的 next 方法不再执行了,也就意味着 checkForComodification 方法不再执行了,所以异常也就不会抛出了。

但是呢,当 List 中有重复元素要删除的时候,break 就不合适了。

for 循环

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
for (int i = 0; i < list.size(); i++) {
	String str = list.get(i);
	if ("1".equals(str)) {
		list.remove(str);
	}
}

for 循环虽然可以避开 fail-fast 保护机制,也就说 remove 元素后不再抛出异常;但是呢,这段程序在原则上是有问题的。为什么呢?

第一次循环的时候,i 为 0,list.size() 为 3,当执行完 remove 方法后,i 为 1,list.size() 却变成了 2,因为 list 的大小在 remove 后发生了变化,也就意味着“2”这个元素被跳过了。能明白吗?

remove 之前 list.get(1) 为“2”;但 remove 之后 list.get(1) 变成了“3”,而 list.get(0) 变成了“2”

使用 Iterator

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");

Iterator<String> itr = list.iterator();

while (itr.hasNext()) {
	String str = itr.next();
	if ("1".equals(str)) {
		itr.remove();
	}
}

为什么使用 Iterator 的 remove 方法就可以避开 fail-fast 保护机制呢?看一下 remove 的源码就明白了。

public void remove() {
    if (lastRet < 0) // 如果没有上一个返回元素的索引,则抛出异常
        throw new IllegalStateException();
    checkForComodification(); // 检查 ArrayList 是否被修改过

    try {
        ArrayList.this.remove(lastRet); // 删除上一个返回元素
        cursor = lastRet; // 更新下一个元素的索引
        lastRet = -1; // 清空上一个返回元素的索引
        expectedModCount = modCount; // 更新 ArrayList 的修改次数
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException(); // 抛出异常
    }
}

删除完会执行 expectedModCount = modCount,保证了 expectedModCount 与 modCount 的同步

总结

在使用 foreach 循环(或称 for-each 循环)遍历集合时,通常不能直接删除集合中的元素,原因如下:

Concurrent Modification Exception:
当使用 foreach 循环遍历集合时,集合的结构不能被修改(例如添加或删除元素),否则会导致 ConcurrentModificationException 异常。这是因为 foreach 循环在背后使用迭代器来遍历集合,而迭代器在遍历时会维护一个 expected modCount(修改计数器),如果在遍历过程中修改了集合的结构,迭代器会检测到并抛出异常。

Invalidation of Iterator:
删除元素后,集合的结构发生变化,这可能会使当前的迭代器失效。如果集合的结构发生了变化,迭代器可能无法正确遍历集合的剩余部分或者导致未定义行为。

Potential Logical Errors:
直接在 foreach 循环内删除元素可能会导致逻辑错误。例如,如果不正确地更新迭代器或集合的大小,可能会导致遍历的元素不完整或错误。

为了安全地从集合中删除元素,应该使用迭代器的 remove() 方法。迭代器的 remove() 方法允许在遍历时安全地删除当前元素,同时更新集合的结构和迭代器的状态,避免了上述问题。

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

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

相关文章

ubnutu20.04-vscode安装leetcode插件流程

1.在vscode插件商城选择安装leetcode 2.安装node.js 官网下载一个版本安装流程&#xff1a; ①tar -xvf node-v14.18.0-linux-x64.tar.xz ①sudo ln -s /app/software/nodejs/bin/npm /usr/local/bin/ ②ln -s /app/software/nodejs/bin/node /usr/local/bin/ 查看版本&…

【vue3|第15期】Vue3模板语法入门指南

日期:2024年7月2日 作者:Commas 签名:(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释:如果您觉得有所帮助,帮忙点个赞,也可以关注我,我们一起成长;如果有不对的地方,还望各位大佬不吝赐教,谢谢^ - ^ 1.01365 = 37.7834;0.99365 = 0.0255 1.02365 = 1377.4083…

sql-语句

文章目录 SQL语句的学习sql是什么sql的内置命令sql的种类sql mode库&#xff0c;表属性介绍&#xff1a;字符集&#xff0c;存储引擎列的数据类型&#xff1a;数字&#xff0c;字符串&#xff0c;时间列的约束DDL: 数据定义语言库表 Online DDL(ALGORITHM) *DML :数据操纵语言资…

如何在Lazada平台快速出单?测评助力商家突破销量瓶颈

Lazada在短短的几年里已经发展成了东南亚地区最大的在线购物网站之一 &#xff0c;很多商家也想要在这样一个大的跨境平台上发展。那么&#xff0c;对于希望在Lazada平台上大展拳脚的商家而言&#xff0c;出单是否容易呢? ​一、Lazada出单容易吗? Lazada出单的难易程度并非…

Linux运维之需掌握的基本Linux命令

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 目录 一、SHELL 二、执行命令 三、常用系统工作命令 四、系统状态检测命令 五、查找定位文件命令 六、文本文件编辑命令 七、文件目录管理命令…

一篇轻松上手 Xorbits库让大规模数据处理更简单.

Xorbits 是一个用于大规模数据处理和计算的 Python 库,旨在提供高效、灵活的分布式计算框架.它类似于Dask 和 Apache Spark,允许用户使用简单的 Python 代码处理大规模数据集. 安装 Xorbits pip install xorbits以下是一个简单的示例,演示如何使用 Xorbits 进行基本的数据处…

网口串口(Serialport)服务器

文章所用工具http://t.csdnimg.cn/2gIR8http://t.csdnimg.cn/2gIR8 搭建服务器界面 操作配置文件保存方式类 public string FileName { get; set; }public IniHelper(string name) {this.FileName name; //在构造函数中给路径赋值} 1 先导入c语言进行读取操作ini文件的方法 …

第三十四篇-学习构建自己的Agent

agentica v0.1 版本升级&#xff1a; https://github.com/shibing624/agentica &#xff08;原项目名&#xff1a;actionflow&#xff09; agentica是一个Agent构建工具&#xff0c;功能&#xff1a; 简单代码快速编排Agent&#xff0c;支持 Reflection(反思&#xff09;、P…

RocketMQ 顺序消息

顺序消息 顺序消息为云消息队列 RocketMQ 版中的高级特性消息&#xff0c;本文为您介绍顺序消息的应用场景、功能原理、使用限制、使用方法和使用建议。 应用场景 在有序事件处理、撮合交易、数据实时增量同步等场景下&#xff0c;异构系统间需要维持强一致的状态同步&#…

LeetCode热题100刷题5:189. 轮转数组、238. 除自身以外数组的乘积、41. 缺失的第一个正数

189. 轮转数组 两次翻转&#xff0c;借助swap实现reverse class Solution { public:void reverse(vector<int>& nums, int left, int right) {int ileft, j right-1;while(i<j) {swap(nums[i],nums[j]);i;j--;}}void rotate(vector<int>& nums, int…

1105 链表合并

solution P1075的简单变形 #include<iostream> #include<vector> #include<algorithm> using namespace std; struct node{int data, next; }list[100000]; int main(){int first1, first2, n, addr;vector<int> l1, l2, ans;scanf("%d%d%d&quo…

[软件安装]linux下安装steam

1、下载安装包到linux系统 SteamTools 发行版 - Gitee.com 2、选择对应的版本 3、解压安装包steam &#xff08;1&#xff09;在opt路径下新建一个文件夹 sudo mkdir steam &#xff08;2&#xff09;进入压缩包路径下&#xff0c;打开终端&#xff0c;执行以下代码进行解压…

使用Fiddler的ImageView轻松获取抓包中的图片详情信息以及一些图片优化建议

使用过Fiddler中的同学是否遇到过下面的问题&#xff1a; 抓包的请求为获取图片信息时&#xff0c;该如何判断图片显示的是什么内容呢&#xff1f;图片是否需要优化来提升前端展示性能呢&#xff1f;常用的图片优化方案有哪些&#xff1f; 本文就带大家搞定上面的这些问题&am…

【VUE3】uniapp + vite中 uni.scss 使用 /deep/ 不生效(踩坑记录三)

vite 中使用 /deep/ 进行样式穿透报错 原因&#xff1a;vite 中不支持&#xff0c;换成 ::v-deep 或:deep即可

echarts阶段仪表图

echarts阶段仪表图 – 效率图 1、先上效果展示 2、完整源码奉上 Vue2 echarts 5 <template><div ref"gaugeChart" style"width: 100%; height: 100%"></div> </template><script> import * as echarts from "echar…

美工画师必看!AI绘画Stable Diffusion 一键生成 B 端图标教程,轻松制作商业可用的设计图标,从此告别加班!(附安装包)

大家好&#xff0c;我是画画的小强 在日常工作中&#xff0c;设计师在应对运营和UI设计的B端图标时&#xff0c;常常面临大量的构思、制作和渲染等工作&#xff0c;耗时耗力。我们可以利用Stable Diffusion(以下简称SD)结合AI的方式&#xff0c;帮助设计师优化图标的设计流程&…

LLM4Decompile——专门用于反编译的大规模语言模型

概述 论文地址&#xff1a;https://arxiv.org/abs/2403.05286 反编译是一种将已编译的机器语言或字节码转换回原始高级编程语言的技术。该技术用于分析软件的内部工作原理&#xff0c;尤其是在没有源代码的情况下&#xff1b;Ghidra 和 IDA Pro 等专用工具已经开发出来&#…

PyCharm社区版Cython支持

自己在文件类型中加一个&#xff0c;名称叫【pythonC】 &#xff0c;文件名模式这一栏要加*.pyx的后缀&#xff0c;之后双击【pythonC】编辑这个文件类型 这里1、2、3、4配置如下 # 1 " # &*, - / : ; <>[ ] { }# 2 False None True and as assert break cdef …

vue 启动项目报错Syntax Error: Error: PostCSS received undefined instead of CSS string

启动vue项目然后报错如下图 这个是跟node版本有关系 因为要开发不同的项目使用不同的node版本&#xff0c;所以就用nvm切换&#xff0c;所以导致了node-sass编译问题 执行这个命令就可以 npm install node-sass or npm rebuild node-sass node-sass对于node高版本和低版本切…

GIT - 一条命令把项目更新到远程仓库

前言 阅读本文大概需要1分钟 说明 更新项目到远程仓库只需要执行一条命令&#xff0c;相当的简便 步骤 第一步 编辑配置文件 vim ~/.bash_profile第二步 写入配置文件 gsh() {local msg"${1:-ADD COMMIT PUSH}"git add . && git commit -m "$m…