0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

HashMap遍历操作为什么不能一边遍历一遍删除呢?

jf_ro2CN3Fa 来源:稀土掘金 2023-02-10 11:25 次阅读

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

ad61d7c6-a8d8-11ed-bfe3-dac502259ad0.png

上面出现这样的原因是在使用 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 代码编译成的字节码,从中揣测它到底是怎么实现的了。

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

publicclassHashMapIteratorDemo{
String[]arr={
"aa",
"bb",
"cc"
};

publicvoidtest1(){
for(Stringstr:arr){}
}
}

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

ad78b9fa-a8d8-11ed-bfe3-dac502259ad0.png

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

publicclassHashMapIteratorDemo2{
String[]arr={
"aa",
"bb",
"cc"
};

publicvoidtest1(){
for(inti=0;i< arr.length; i++) {
            String str = arr[i];
        }
    }
}
ad8ca438-a8d8-11ed-bfe3-dac502259ad0.png

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

通过 foreach 遍历集合:

publicclassHashMapIteratorDemo3{
List< Integer >list=newArrayList< >();

publicvoidtest1(){
list.add(1);
list.add(2);
list.add(3);

for(Integer
var:list){}
}
}

通过 Iterator 遍历集合:

publicclassHashMapIteratorDemo4{
List< Integer >list=newArrayList< >();

publicvoidtest1(){
list.add(1);
list.add(2);
list.add(3);

Iterator< Integer >it=list.iterator();
while(it.hasNext()){
Integer
var=it.next();
}
}
}

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

ad9fa5e2-a8d8-11ed-bfe3-dac502259ad0.pngadb73e6e-a8d8-11ed-bfe3-dac502259ad0.png

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

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

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

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

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

1、现象

根据以上分析,我们知道 HashMap 底层是实现了 Iterator 迭代器的 ,那么理论上我们也是可以使用迭代器进行遍历的,这倒是不假,例如下面:

publicclassHashMapIteratorDemo5{
publicstaticvoidmain(String[]args){
Map< Integer, String >map=newHashMap< >();
map.put(1,"aa");
map.put(2,"bb");
map.put(3,"cc");

for(Map.Entry< Integer, String >entry:map.entrySet()){
intk=entry.getKey();
Stringv=entry.getValue();
System.out.println(k+"="+v);
}
}
}

输出:

adcdbe00-a8d8-11ed-bfe3-dac502259ad0.png

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

publicclassHashMapIteratorDemo5{
publicstaticvoidmain(String[]args){
Map< Integer, String >map=newHashMap< >();
map.put(1,"aa");
map.put(2,"bb");
map.put(3,"cc");

for(Map.Entry< Integer, String >entry:map.entrySet()){
intk=entry.getKey();
if(k==1){
map.put(1,"AA");
}
Stringv=entry.getValue();
System.out.println(k+"="+v);
}
}
}

执行结果:

ade3cc72-a8d8-11ed-bfe3-dac502259ad0.png

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

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

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

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

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

adf7e900-a8d8-11ed-bfe3-dac502259ad0.png

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

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

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

publicclassHashMapIteratorDemo5{
publicstaticvoidmain(String[]args){
Map< Integer, String >map=newHashMap< >();
map.put(1,"aa");
map.put(2,"bb");
map.put(3,"cc");

for(Map.Entry< Integer, String >entry:map.entrySet()){
intk=entry.getKey();
if(k==1){
map.put(4,"AA");
}
Stringv=entry.getValue();
System.out.println(k+"="+v);
}
}
}

执行出现异常:

ae0aa3ec-a8d8-11ed-bfe3-dac502259ad0.png

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

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

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

2、细究底层原理

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

finalNode< K, V >nextNode(){
Node< K, V >[]t;
Node< K, V >e=next;
if(modCount!=expectedModCount)
thrownewConcurrentModificationException();
if(e==null)
thrownewNoSuchElementException();
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 实现:

ae228c78-a8d8-11ed-bfe3-dac502259ad0.png

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

(2)HashMap.KeySet 的 remove 实现

publicfinalbooleanremove(Objectkey){
returnremoveNode(hash(key),key,null,false,true)!=null;
}

(3)HashMap.EntrySet 的 remove 实现

publicfinalbooleanremove(Objecto){
if(oinstanceofMap.Entry){
Map.Entry<< ? , ? >e=(Map.Entry<< ? , ? >)o;
Objectkey=e.getKey();
Objectvalue=e.getValue();
returnremoveNode(hash(key),key,value,true,true)!=null;
}
returnfalse;
}

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

publicfinalvoidremove(){
Node< K, V >p=current;
if(p==null)
thrownewIllegalStateException();
if(modCount!=expectedModCount)
thrownewConcurrentModificationException();
current=null;
Kkey=p.key;
removeNode(hash(key),key,null,false,false);
expectedModCount=modCount;//--这里将expectedModCount与modCount进行同步
}

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

finalNode< K, V >removeNode(inthash,Objectkey,Objectvalue,
booleanmatchValue,booleanmovable){
Node< K, V >[]tab;
Node< K, V >p;
intn,index;
if((tab=table)!=null&&(n=tab.length)>0&&
...
if(node!=null&&(!matchValue||(v=node.value)==value||
(value!=null&&value.equals(v)))){
if(nodeinstanceofTreeNode)
((TreeNode< K, V >)node).removeTreeNode(this,tab,movable);
elseif(node==p)
tab[index]=node.next;
else
p.next=node.next;
++modCount;//----这里对modCount进行了自增,可能会导致后面与expectedModCount不一致
--size;
afterNodeRemoval(node);
returnnode;
}
}
returnnull;
}

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

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

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

publicclassHashMapIteratorDemo5{
publicstaticvoidmain(String[]args){
Map< Integer, String >map=newHashMap< >();
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();
intkey=entry.getKey();
if(key==1){
it.remove();
}
}
}
}






审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 编译器
    +关注

    关注

    1

    文章

    1624

    浏览量

    49114
  • JAVA语言
    +关注

    关注

    0

    文章

    138

    浏览量

    20090
  • hashmap
    +关注

    关注

    0

    文章

    14

    浏览量

    2285

原文标题:HashMap 为什么不能一边遍历一遍删除

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    为什么L298n的输出端本来是一边一边低的,

    为什么L298n的输出端本来是一边一边低的,接上直流电机后,两端的电压就变了,就在跳动,0到4.3之间跳动:
    发表于 08-09 14:53

    手机一边充电一边使用对人,手机,电池有哪些危害

    手机一边充电一边使用对人,手机,电池有哪些危害
    发表于 08-04 09:43

    手机如何实现一边充电一边听歌(听)

    ,如何实现一边充电一边听歌(听)?因此,USB Type-C接口的转接器应时而生了,轻松的实现不同音频插头与音响耳机之间的相互转换,
    发表于 09-14 08:41

    如何通过media graph遍历entity?

    如何通过media graph遍历entity?
    发表于 03-10 07:04

    Merkle树遍历技术的研究

    Merkle树应用于数字加密技术。它的遍历技术主要包含树的根节点生成和认证路径的生成。本文主要比较各种Merkle树的遍历技术,提出自己的遍历方法,并进行了实验仿真和对实验结果的
    发表于 03-01 16:16 14次下载

    二叉树的前序遍历、中序遍历、后续遍历的非递归实现

    前序遍历:先访问该节点,然后访问该节点的左子树和右子树; 中序遍历:先访问该节点的左子树,然后访问该节点,再访问该节点的右子树; 后序遍历:想访问该节点的左子树和右子树,然后访问该节点。
    发表于 11-27 11:24 1133次阅读

    jquery的each遍历方法

    本文为大家介绍Jquery中each的三种遍历方法,有兴趣的伙伴可以参考下。
    发表于 12-03 10:19 2558次阅读

    图不同存储方式的应用和遍历操作及综合应用资料说明

    、 实验目的:1.掌握图的不同存储方式的应用;2. 掌握图的遍历操作。 二、 实验要求:1.用C语言实现源程序的编写;2.源程序应书写规范,要采用缩进格式,适当添加注释;3.程序要具有
    发表于 03-11 08:00 4次下载
    图不同存储方式的应用和<b class='flag-5'>遍历</b><b class='flag-5'>操作</b>及综合应用资料说明

    螺旋遍历二维数组漫画讲解

    来自公众号:程序员小灰 第二天 什么意思?我们来举个例子,给定下面这样个二维数组: 我们需要从左上角的元素1开始,按照顺时针进行螺旋遍历
    的头像 发表于 11-26 14:01 1751次阅读

    Java的iterator和foreach遍历集合源代码

    Java的iterator和foreach遍历集合源代码
    发表于 03-17 09:16 9次下载
    Java的iterator和foreach<b class='flag-5'>遍历</b>集合源代码

    二叉树的前序遍历非递归实现

    我们之前说了二叉树基础及二叉的几种遍历方式及练习题,今天我们来看下二叉树的前序遍历非递归实现。 前序遍历的顺序是, 对于树中的某节点,先遍历
    的头像 发表于 05-28 13:59 1956次阅读

    总结下OpenCV遍历图像的几种方法

    在图形处理中,遍历每个像素点是最基本的功能,是做算法的基础,这篇文章来总结下OpenCV遍历图像的几种方法。
    的头像 发表于 01-18 15:08 1727次阅读

    为什么很少有人按列去遍历访问二维数组

    二维数组大家都很熟悉,正常人遍历二维数组都是行来的,为什么很少有人按列去遍历
    的头像 发表于 02-12 09:47 714次阅读
    为什么很少有人按列去<b class='flag-5'>遍历</b>访问二维数组<b class='flag-5'>呢</b>?

    如何遍历中文字符串

    今天和大家分享下如何遍历中文字符串,主要是如何打印中文字符,因为中文字符串每个字符占用不只个字节的空间,如果我们逐个字节遍历,会出现奇怪的结果。而UTF-8编码写的中文字符是有特定结构的,我们可以
    的头像 发表于 07-03 09:15 694次阅读
    如何<b class='flag-5'>遍历</b>中文字符串

    python如何遍历列表并提取

    遍历列表是Python中非常常见的操作,可以使用for循环或者while循环来实现。下面我将详细介绍如何使用for循环遍历列表并提取元素。 首先,让我们简单了解
    的头像 发表于 11-23 15:55 1378次阅读