HashMap 为什么不能一边遍历一遍删除

news2024/11/15 17:43:08

前段时间,同事在代码中 KW 扫描的时候出现这样一条:

上面出现这样的原因是在使用 foreach 对 HashMap 进行遍历时,同时进行 put 赋值操作会有问题,异常 ConcurrentModificationException。

于是帮同简单的看了一下,印象中集合类在进行遍历时同时进行删除或者添加操作时需要谨慎,一般使用迭代器进行操作。

于是告诉同事,应该使用迭代器 Iterator 来对集合元素进行操作。同事问我为什么?这一下子把我问蒙了?对啊,只是记得这样用不可以,但是好像自己从来没有细究过为什么?

于是今天决定把这个 HashMap 遍历操作好好地研究一番,防止采坑!

foreach 循环?

Java foreach 语法是在 JDK 1.5 时加入的新特性,主要是当作 for 语法的一个增强,那么它的底层到底是怎么实现的呢?下面我们来好好研究一下:

foreach 语法内部,对 collection 是用 iterator 迭代器来实现的,对数组是用下标遍历来实现。Java 5 及以上的编译器隐藏了基于 iteration 和数组下标遍历的内部实现。

注意:这里说的是“Java 编译器”或 Java 语言对其实现做了隐藏,而不是某段 Java 代码对其实现做了隐藏,也就是说,我们在任何一段 JDK 的 Java 代码中都找不到这里被隐藏的实现。这里的实现,隐藏在了Java 编译器中,查看一段 foreach 的 Java 代码编译成的字节码,从中揣测它到底是怎么实现的了。

我们写一个例子来研究一下:

public class HashMapIteratorDemo {    String[] arr = {        "aa",        "bb",        "cc"    };
    public void test1() {        for (String str: arr) {}    }}
 

将上面的例子转为字节码反编译一下(主函数部分):

也许我们不能很清楚这些指令到底有什么作用,但是我们可以对比一下下面段代码产生的字节码指令:

 

public class HashMapIteratorDemo2 {    String[] arr = {        "aa",        "bb",        "cc"    };
    public void test1() {        for (int i = 0; i < arr.length; i++) {            String str = arr[i];        }    }}

看看两个字节码文件,有木有发现指令几乎相同,如果还有疑问我们再看看对集合的 foreach 操作:

通过 foreach 遍历集合:

 

public class HashMapIteratorDemo3 {    List < Integer > list = new ArrayList < > ();
    public void test1() {        list.add(1);        list.add(2);        list.add(3);
        for (Integer            var: list) {}    }}

通过 Iterator 遍历集合:

 

public class HashMapIteratorDemo4 {    List < Integer > list = new ArrayList < > ();
    public void test1() {        list.add(1);        list.add(2);        list.add(3);
        Iterator < Integer > it = list.iterator();        while (it.hasNext()) {            Integer            var = it.next();        }    }}

将两个方法的字节码对比如下:

我们发现两个方法字节码指令操作几乎一模一样;

这样我们可以得出以下结论:

对集合来说,由于集合都实现了 Iterator 迭代器,foreach 语法最终被编译器转为了对 Iterator.next() 的调用;

对于数组来说,就是转化为对数组中的每一个元素的循环引用。

HashMap 遍历集合并对集合元素进行 remove、put、add

1、现象

根据以上分析,我们知道 HashMap 底层是实现了 Iterator 迭代器的 ,那么理论上我们也是可以

使用迭代器进行遍历的,这倒是不假,例如下面:

 

public class HashMapIteratorDemo5 {    public static void main(String[] args) {        Map < Integer, String > map = new HashMap < > ();        map.put(1, "aa");        map.put(2, "bb");        map.put(3, "cc");
        for (Map.Entry < Integer, String > entry: map.entrySet()) {            int k = entry.getKey();            String v = entry.getValue();            System.out.println(k + " = " + v);        }    }}

输出:

OK,遍历没有问题,那么操作集合元素 remove、put、add 呢?

 

public class HashMapIteratorDemo5 {    public static void main(String[] args) {        Map < Integer, String > map = new HashMap < > ();        map.put(1, "aa");        map.put(2, "bb");        map.put(3, "cc");
        for (Map.Entry < Integer, String > entry: map.entrySet()) {            int k = entry.getKey();            if (k == 1) {                map.put(1, "AA");            }            String v = entry.getValue();            System.out.println(k + " = " + v);        }    }}

执行结果:

执行没有问题,put 操作也成功了。

但是!但是!但是!问题来了!!!

我们知道 HashMap 是一个线程不安全的集合类,如果使用 foreach 遍历时,进行add, remove 操作会 java.util.ConcurrentModificationException 异常。put 操作可能会抛出该异常。(为什么说可能,这个我们后面解释)

为什么会抛出这个异常呢?

我们先去看一下 Java API 文档对 HasMap 操作的解释吧。

翻译过来大致的意思就是:该方法是返回此映射中包含的键的集合视图。

集合由映射支持,如果在对集合进行迭代时修改了映射(通过迭代器自己的移除操作除外),则迭代的结果是未定义的。集合支持元素移除,通过 Iterator.remove、set.remove、removeAll、retainal 和 clear 操作从映射中移除相应的映射。简单说,就是通过 map.entrySet() 这种方式遍历集合时,不能对集合本身进行 remove、add 等操作,需要使用迭代器进行操作。

对于 put 操作,如果这个操作时替换操作如上例中将第一个元素进行修改,就没有抛出异常,但是如果是使用 put 添加元素的操作,则肯定会抛出异常了。我们把上面的例子修改一下:

public class HashMapIteratorDemo5 {    public static void main(String[] args) {        Map < Integer, String > map = new HashMap < > ();        map.put(1, "aa");        map.put(2, "bb");        map.put(3, "cc");
        for (Map.Entry < Integer, String > entry: map.entrySet()) {            int k = entry.getKey();            if (k == 1) {                map.put(4, "AA");            }            String v = entry.getValue();            System.out.println(k + " = " + v);        }    }}

执行出现异常:

这就是验证了上面说的 put 操作可能会抛出 java.util.ConcurrentModificationException 异常。

但是有疑问了,我们上面说过 foreach 循环就是通过迭代器进行的遍历啊?为什么到这里是不可以了呢?

这里其实很简单,原因是我们的遍历操作底层确实是通过迭代器进行的,但是我们的 remove 等操作是通过直接操作 map 进行的,如上例子:map.put(4, "AA"); //这里实际还是直接对集合进行的操作,而不是通过迭代器进行操作。所以依然会存在 ConcurrentModificationException 异常问题。

2、细究底层原理

我们再去看看 HashMap 的源码,通过源代码,我们发现集合在使用 Iterator 进行遍历时都会用到这个方法:

final Node < K, V > nextNode() {    Node < K, V > [] t;    Node < K, V > e = next;    if (modCount != expectedModCount)        throw new ConcurrentModificationException();    if (e == null)        throw new NoSuchElementException();    if ((next = (current = e).next) == null && (t = table) != null) {        do {} while (index < t.length && (next = t[index++]) == null);    }    return e;}

这里 modCount 是表示 map 中的元素被修改了几次(在移除,新加元素时此值都会自增),而 expectedModCount 是表示期望的修改次数,在迭代器构造的时候这两个值是相等,如果在遍历过程中这两个值出现了不同步就会抛出 ConcurrentModificationException 异常。

现在我们来看看集合 remove 操作:

(1)HashMap 本身的 remove 实现:

 

public V remove(Object key) {    Node < K, V > e;    return (e = removeNode(hash(key), key, null, false, true)) == null ?        null : e.value;}

(2)HashMap.KeySet 的 remove 实现

 

public final boolean remove(Object key) {    return removeNode(hash(key), key, null, false, true) != null;}

(3)HashMap.EntrySet 的 remove 实现

 

public final boolean remove(Object o) {    if (o instanceof Map.Entry) {        Map.Entry << ? , ? > e = (Map.Entry << ? , ? > ) o;        Object key = e.getKey();        Object value = e.getValue();        return removeNode(hash(key), key, value, true, true) != null;    }    return false;}

(4)HashMap.HashIterator 的 remove 方法实现

public final void remove() {    Node < K, V > p = current;    if (p == null)        throw new IllegalStateException();    if (modCount != expectedModCount)        throw new ConcurrentModificationException();    current = null;    K key = p.key;    removeNode(hash(key), key, null, false, false);    expectedModCount = modCount; //--这里将expectedModCount 与modCount进行同步}

以上四种方式都通过调用 HashMap.removeNode 方法来实现删除key的操作。在 removeNode 方法内只要移除了 key, modCount 就会执行一次自增操作,此时 modCount 就与 expectedModCount 不一致了;

final Node < K, V > removeNode(int hash, Object key, Object value,    boolean matchValue, boolean movable) {    Node < K, V > [] tab;    Node < K, V > p;    int n, index;    if ((tab = table) != null && (n = tab.length) > 0 &&        ...        if (node != null && (!matchValue || (v = node.value) == value ||                (value != null && value.equals(v)))) {            if (node instanceof TreeNode)                ((TreeNode < K, V > ) node).removeTreeNode(this, tab, movable);            else if (node == p)                tab[index] = node.next;            else                p.next = node.next;            ++modCount; //----这里对modCount进行了自增,可能会导致后面与expectedModCount不一致            --size;            afterNodeRemoval(node);            return node;        }    }    return null;}

上面三种 remove 实现中,只有第三种 iterator 的 remove 方法在调用完 removeNode 方法后同步了 expectedModCount 值与 modCount 相同,所以在遍历下个元素调用 nextNode 方法时,iterator 方式不会抛异常。

到这里是不是有一种恍然大明白的感觉呢!

所以,如果需要对集合遍历时进行元素操作需要借助 Iterator 迭代器进行,如下:

public class HashMapIteratorDemo5 {    public static void main(String[] args) {        Map < Integer, String > map = new HashMap < > ();        map.put(1, "aa");        map.put(2, "bb");        map.put(3, "cc");
        Iterator < Map.Entry < Integer, String >> it = map.entrySet().iterator();        while (it.hasNext()) {            Map.Entry < Integer, String > entry = it.next();            int key = entry.getKey();            if (key == 1) {                it.remove();            }        }    }}

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

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

相关文章

为什么Qt成为工业软件开发的首选框架?

工业软件开发中使用Qt的主要原因有以下几点&#xff1a; 跨平台性&#xff1a;Qt是一个跨平台的C应用程序开发框架&#xff0c;可以在多个操作系统上运行&#xff0c;包括Windows、macOS、Linux等。这种跨平台性使得开发人员能够使用相同的代码库创建适用于不同操作系统的应用程…

Github-Readme-Stats 简明教程

注&#xff1a;本篇文章首发于 博客园sarexpine 为保持阅读的舒适性&#xff0c;可以选择移步至 博客园sarexpine 阅读&#xff0c;日后的文章将首发于 博客园sarexpine 平台&#xff0c;谢谢&#xff01; 1. 更新 Readme 文件 在你想要放置 Readme-WakaTime 的位置中放置以下…

Nginx【Nginx场景实践(动静分离、动静分离实战、缓存机制)】(十)-全面详解(学习总结---从入门到深化)

目录 Nginx场景实践_动静分离 Nginx场景实践_动静分离实战 Nginx场景实践_缓存机制 Nginx场景实践_动静分离 Nginx动静分离简单来说就是把动态和静态请求分开&#xff0c;不能理解成只是单纯的把动态页面和静态页面物理分离。严格意义上说应该是动态请求和静态请求分开&…

@Transaction事务导致的mysql连接耗尽源码分析

背景&#xff1a; Transaction注解是我们在日常的写代码过程中最常使用的事务注解了&#xff0c;本文就从spring源码的角度解析下这个注解的执行过程&#xff0c;以便分析为什么使用事务比正常的单sql执行更容易导致连接池耗尽 源码追踪&#xff1a; 本文假定使用PROPAGATIO…

音频怎么转文字?试试这三个简单的方法吧!

有时候我们会遇到一些比较重要的会议、讲座或者演讲&#xff0c;如果我们能够将这些声音记录下来&#xff0c;并将其转换为文字&#xff0c;就可以更加方便地保存和管理这些信息。这样&#xff0c;我们就不用再去听录音了&#xff0c;只需要看文字就可以了。此外&#xff0c;录…

智能指针的deleter机制

一、介绍 智能指针的deleter机制是指&#xff0c;当智能指针的引用计数降为0时&#xff0c;智能指针会自动调用一个指定的析构函数&#xff08;deleter&#xff09;来释放所管理的内存。这个析构函数通常是一个函数对象&#xff0c;可以是一个函数指针、一个lambda表达式或者一…

青少年机器人技术一级考试备考重点(四):功与能量以及常用传动装置

随着机器人技术的飞速发展&#xff0c;越来越多的青少年开始关注并参与其中。青少年机器人技术考试作为一项评估学生机器人技术水平的重要考试&#xff0c;备受广大青少年和家长的关注。为了更好地备战青少年机器人技术一级考试&#xff0c;了解考试的学习要点和备考重点是非常…

C++数据结构X篇_07_C++单向循环链表解决约瑟夫问题

本篇参考单向循环链表解决约瑟夫问题&#xff08;C&#xff09;整理&#xff0c;先搞懂结构框架&#xff0c;后期根据视频利用c对内容实现&#xff0c;也可以对c有更高的提升。 文章目录 1. 链表创建与初始化2. 添加插入、删除和打印函数3. 插入数据并核验4. 解决约瑟夫问题&am…

DM8:达梦数据库备份还原报错-文件已存在 -4558 file exists

DM8:达梦数据库备份还原报错-文件已存在 -4558 file exists 1 文件已存在 -4558 file exists2 使用 OVERWRITE 参数对数据库还原3 参数介绍 1 文件已存在 -4558 file exists 在数据库还原操作时&#xff0c;遇到报错文件已存在 -4558 file exists&#xff0c;可以使用OVERWRIT…

IP归属地与IP定位

IP归属地查询是指根据给定的IP地址&#xff0c;确定该IP地址所属的地理位置或网络服务提供商。这种查询可以帮助用户了解到访者的地理位置&#xff0c;有助于网络安全、反垃圾邮件等应用。 在实际应用中&#xff0c;IP归属地查询常用于以下几个方面&#xff1a; 网络安全&…

vcruntime140_1.dll详细修复方法(推荐使用这个方法)

vcruntime140_1.dll丢失要怎么办&#xff1f;其实很多人都在头疼这个问题&#xff0c;关于dll文件的丢失这事情是时常发生的&#xff0c;因为电脑的杀毒软件有时候会误杀&#xff0c;然后就会导致你的游戏程序都打开不了&#xff0c;你必须要修复好了才行&#xff0c;今天小编就…

【UE5 Cesium】09-Cesium for Unreal 子关卡应用实例(下)

效果 通过按钮点击事件实现子关卡的切换 步骤 新建两个Actor蓝图作为GeoMarker&#xff0c;分别命名为“BP_GeoMarker_BeiJing”、“BP_GeoMarker_ShangHai” 分别打开这两个蓝图&#xff0c;添加文本渲染组件 在指定的地理位置上拖入蓝图“BP_GeoMarker_BeiJing” 控制“BP_…

分布式缓存系统热点数据

一、背景 分布式缓存一般被定义为一个数据集合&#xff0c;它将数据分布&#xff08;或分区&#xff09;于任意数目的集群节点上。集群中的一个具体节点负责缓存中的一部分数据&#xff0c;整体对外提供统一的访问接口 Amazon 于 2007 年提出的一种改进的一致性哈希算法 [4]。…

华为OD机试真题 Java 实现【查找单入口空闲区域】【2022 Q4 100分】,附详细解题思路

目录 一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明 一、题目描述 给定一个 m x n 的矩阵&#xff0c;由若干字符 ‘X’ 和 ‘O’构成&#xff0c;’X’表示该处已被占据&#xff0c;’O’表示该处空闲&#xff0c;请找…

【JS】将表格数据下载为 .csv 文件

文章目录 代码实现 代码实现 1. 将表格数据转换为字符串格式 2. 字符串格式里面的,逗号表示换列 3. 字符串格式里面的\n符号表示换行实现 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><…

Linux(包括centos) 如何查看服务器内存、CPU

CPU架构 CPU架构主要包括&#xff1a;amd64、arm32v7、arm64v8、mips64el、mips32、ppc64le和ppc32等架构。 CPU信息 CPU信息主要为中央处理器详细信息&#xff0c;包括&#xff1a; 架构核心数量处理速度厂商名称CPU主频标签 … 注&#xff1a;不同的操作系统或者CPU架构提供…

怎么提取视频中的音频?这些提取音频方法很简单

将视频中的音频提取出来&#xff0c;可以单独对音频进行处理&#xff0c;如剪辑、增强声音等&#xff0c;而不影响视频本身的内容。在后期制作中&#xff0c;音频需要经过一系列的处理&#xff0c;如去噪、降噪、混响等&#xff0c;提取出音频可以更方便地进行这些处理&#xf…

MHA高可用

文章目录 MHA高可用1 定义2 组成3 特点4 搭建MySQL MHA4.1 配置主从复制4.2 关闭防火墙&#xff0c;安全机制4.3 修改Master、Slave1、Slave2节点的主机名4.4 添加主机映射关系4.5 修改 Master、Slave1、Slave2 节点的 Mysql主配置文件/etc/my.cnf4.6 在master、slave1、slave2…

2023年--上半年小程序团队工作总结

前言 大家好&#xff0c;这是小程序团队第一次跟大家见面。小程序团队从2020年开始&#xff0c;就着手进行着小程序的开发。 在2020年7月上线了第一个现在仍在使用的上古小程序&#xff1a;课程小程序 和 我的内容库小程序。 小伙伴们可能还不知道&#xff0c;你们平时日常在cs…

10分钟看透微信公众号支付

开发痛点 如何配置微信&#xff1f;怎么个流程&#xff1f;怎么入手&#xff1f;如何本地调试&#xff1f;网上教程10个小时不想看怎么办&#xff1f;这里一篇文章带你入手微信支付。看看微信公众号支付到底有什么神奇之处。 开发后结果 微信配置 1、首先打开文档中心&#…