设为首页 加入收藏

TOP

公司新来一个同事:为什么 HashMap 不能一边遍历一边删除?一下子把我问懵了!(一)
2023-07-25 21:38:23 】 浏览:57
Tags:司新来 HashMap 能一边 边删除

作者:你呀不牛
链接:https://juejin.cn/post/7114669787870920734

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

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

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

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

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

foreach循环?

java foreach 语法是在jdk1.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操作的解释吧。

翻译过来大致的意思就是该方法是返回此映射中包含的键的集合视图。集合由映射支持,如果在对集合进行迭代时修改了映射(通过迭代器自己的移除操作除外),则迭代的结果是未定义的。集合支持元素移

首页 上一页 1 2 3 下一页 尾页 1/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇三天吃透计算机网络面试八股文 下一篇 推荐一套轻量级的开源图床系统:..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目