深入HashMap底层理解阿里手册的遍历守则

写在文章开头

你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,这里面会有笔者精心挑选的并发、JVM、MySQL数据库专栏,也有笔者日常分享的硬核技术小文。

在这里插入图片描述

HashMap算是我们日常开发中比较常用的一个集合工具,在阿里开发手册中也有提及关于HashMap的使用守则:

  【推荐】使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历。说明:keySet其实是遍历了2次,一次是转为Iterator对象,另一次是从hashMap中取出key所对应的value。而entrySet只是遍历了一次就把key和value都放到了entry中,效率更高。如果是JDK8,使用Map.foreach方法。正例:values()返回的是V值集合,是一个list集合对象;keySet()返回的是K值集合,是一个Set集合对象;entrySet()返回的是K-V值组合集合。

对于这一守则,笔者会通过代码实验和HashMap底层实现进行一次深入剖析,希望能够让读者对该守则有更深刻的理解和掌握。

在这里插入图片描述

实验论证守则

在论证为什么之前,我们必须先印证是不是,所以笔者针对这一守则给出两段示例代码,先来看看基于keySet 遍历的代码示例,可以看到笔者为了让增加散列的随机性,key使用工具类进行随机生成20长度的字符串:

public static void main(String[] args) {//生成2000w的数据Map<String, Integer> map = new HashMap<>();for (int i = 0; i < 2000_0000; i++) {map.put(RandomUtil.randomString(20), i);}//使用keySet取值long begin = System.currentTimeMillis();Set<String> keySet = map.keySet();for (String key : keySet) {map.get(key);}long end = System.currentTimeMillis();log.info("使用keySet 总耗时:{}ms", end - begin);}

对应输出结果如下,可以看到总耗时为816ms:

23:55:57.159 [main] INFO com.sharkChili.webTemplate.test.Test - 使用keySet 总耗时:816ms

同样的我们给出entrySet的示例:

public static void main(String[] args) {//生成2000w的数据Map<String, Integer> map = new HashMap<>();for (int i = 0; i < 2000_0000; i++) {map.put(RandomUtil.randomString(20), i);}//使用 entrySet 取值long begin = System.currentTimeMillis();Set<Map.Entry<String, Integer>> entrySet = map.entrySet();for (Map.Entry<String, Integer> entry : entrySet) {entry.getValue();}long end = System.currentTimeMillis();log.info("使用entrySet 总耗时:{}ms", end - begin);}

对应输出结果如下,可以看到耗时为keySet一半左右,不难看出同样时遍历前者遍历时比后者多做了一倍的工作:

23:57:26.593 [main] INFO com.sharkChili.webTemplate.test.Test - 使用entrySet 总耗时:482ms

源码剖析

当我们调用keySet()获取keySe集合时,底层实际上是为当前这个HashMap生成一个keySet对象,这一点我们从注释中也能看到这一点,自此我们可知这个视图并不是调用时遍历生成的:

Returns a {@link Set} view of the keys contained in this map.

其次我们查看源码,发现进行keySet初始化创建时,会将其赋值给成员变量ks ,这也就意味着后续在使用keySet进行遍历时,HashMap就不会再创建一个全新的keySet

 public Set<K> keySet() {Set<K> ks = keySet;//初始化时创建一个key的视图,后续使用这个视图时都是复用本次初始化得来的if (ks == null) {ks = new KeySet();keySet = ks;}return ks;

随后我们在使用增强for进行遍历时,代码会来到内部迭代器HashIterator 的内部类KeyIterator ,为当前遍历创建一个KeyIterator

abstract class HashIterator {final class KeyIterator extends HashIteratorimplements Iterator<K> {public final K next() { return nextNode().key; }}
}

而这个迭代器是继承自HashIterator,所以进行初始化时,实际上还是调用HashIterator的构造方法。
我们都知道HashMap底层的实现是数组+链表/红黑树,所以在进行key的迭代器生成时,它会通过遍历找到数组中第一个不为空的位置,并让next指针指向这个元素:

在这里插入图片描述

对应的源码如下图所示,HashIteratordo-while循环中不断步进直到next指向的索引位置不为空为止:

 HashIterator() {expectedModCount = modCount;Node<K,V>[] t = table;current = next = null;index = 0;//不断步进这个底层数组table,直到找到第一个不为空的元素为止if (t != null && size > 0) { // advance to first entrydo {} while (index < t.length && (next = t[index++]) == null);}}

完成迭代器的初始化工作后,增强for取值逻辑就是基于这个KeyIteratorhasNext(实际调用的是其父类HashIteratorhasNext):

 public final boolean hasNext() {return next != null;}

只要上述方法不为空,迭代器则直接将这个key对应的节点Nodekey返回出去:

final class KeyIterator extends HashIteratorimplements Iterator<K> {public final K next() { return nextNode().key; }}

随后我们又回过头用这个keyHashMap使用get方法获取value,可以看到如果当前节这个key是链表或者红黑树的话,又需要进行一次**O(n)或者O(log n)**的查询:

final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;//红黑树的遍历定位if ((e = first.next) != null) {if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);//链表的循环定位   do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;}

以下图为例,假设我们要获取key为b的value,这就意味着,我们迭代keySet时经过了a,b,在获取value时要需要根据key定位到索引1的位置,再经过一次a节点找到key为b的节点,自此我们也就直到了为什么使用keySet定位k-v效率低下的原因了。

在这里插入图片描述

了解了keySet的低效,我们自然也需要直到为什么entrySet是高效的,和keySet一样,entrySet进行初始化时也是创建一个EntrySet对象并赋值给成员变量es 。它同样也是HashIterator的子类,所以初始化next定位第一个元素的步骤和keySet是一样的,这里就不多赘述了。

唯一区别就是我们的使用entrySet进行遍历时,返回的就是HashMap的节点entry,所以我们在获取value时无需再回过头到数组中定位元素,避免了一次扫描,这也是为什么是使用entrySet进行k-v遍历高效的原因所在。

final class EntryIterator extends HashIteratorimplements Iterator<Map.Entry<K,V>> {public final Map.Entry<K,V> next() { return nextNode(); }}

小结

相信通过笔者的解读,你对于HashMap的底层实现和遍历技巧有着更进一步的理解和掌握,基于此文笔者也顺便总结一下HashMap的一些遍历守则:

  1. 只需要遍历key时用keySet方法。
  2. 只要valuesvalues方法。
  3. 若需要遍历k-v则建议是使用entrySet或者JDK8提供的forEach方法。

我是sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号:
写代码的SharkChili,同时我的公众号也有我精心整理的并发编程JVMMySQL数据库个人专栏导航。

在这里插入图片描述

参考

为什么阿里巴巴为什么不推荐使用keySet()进行遍历HashMap?:https://juejin.cn/post/7295353579002396726

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

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

相关文章

特斯拉FSD的神经网络(Tesla 2022 AI Day)

这是特斯拉的全自动驾驶&#xff08;Full Self Driver&#xff09;技术结构图&#xff0c;图中把自动驾驶模型拆分出分成了几个依赖的模块&#xff1a; 技术底座&#xff1a;自动标注技术处理大量数据&#xff0c;仿真技术创造图片数据&#xff0c;大数据引擎进不断地更新&…

Visual Studio2022实用使用技巧集

前言 对于.NET开发者而言Visual Studio是我们日常工作中比较常用的开发工具&#xff0c;掌握一些Visual Studio实用的搜索、查找、替换技巧可以帮助我们大大提高工作效率从而避免996。 Visual Studio更多实用技巧 https://github.com/YSGStudyHards/DotNetGuide 代码和功能搜…

用flinkcdc debezium来捕获数据库的删除内容

我在用flinkcdc把数据从sqlserver写到doris 正常情况下sqlserver有删除数据&#xff0c;doris是能捕获到并很快同步删除的。 但是我现在情况是doris做为数仓&#xff0c;数据写到ods&#xff0c;ods的数据还会通过flink计算后写入dwd层&#xff0c;所以此时ods的数据是删除了…

【解决方案】浅谈安科瑞无线测温监控系统方案

1概述 Acrel-2000T无线测温监控系统装置适用于高低压开关柜内电缆接头、断路器触头、刀闸开关、高压电缆中间头、干式变压器、低压大电流等设备的温度监测&#xff0c;防止在运行过程中因氧化、松动、灰尘等因素造成接点接触电阻过大而发热成为安全隐患&#xff0c;提高设备安…

用ChatGPT教学、科研!亚利桑那州立大学与OpenAI合作

亚利桑那州立大学&#xff08;简称“ASU”&#xff09;在官网宣布与OpenAI达成技术合作。从2024年2月份开始&#xff0c;为所有学生提供ChatGPT企业版访问权限&#xff0c;主要用于学习、课程作业和学术研究等。 为了帮助学生更好地学习ChatGPT和大语言模型产品&#xff0c;AS…

3DMAX初级小白班第一课:菜单栏介绍

基本介绍 这里不可能一个一个选项全部教给大家&#xff08;毕竟之后靠实操慢慢就记住了&#xff09;&#xff0c;只说一些相对需要注意的设置。 自定义-热键编辑器-热键设置 这里有你所需要的全部快捷键 自定义-自定义UI启动布局 将UI布局还原到启动的位置 自定义-通用单…

第2章-OSI参考模型与TCP/IP模型

目录 1. 引入 2. OSI参考模型 2.1. 物理层 2.2. 数据链路层 2.3. 网络层 2.4. 传输层 2.5. 会话层 2.6. 表示层 2.7. 应用层 3. 数据的封装与解封装 4. TCP/IP模型 4.1. 背景引入 4.2. TCP/IP模型&#xff08;4层&#xff09; 4.3. 拓展 1. 引入 1&#xff09;产…

Maven 打包时,依赖配置正确,但是类引入出现错误,一般是快照(Snapshot)依赖拉取策略问题

问题描述&#xff1a; 项目打包时&#xff0c;类缺少依赖&#xff0c;操作 pom.xml -> Maven -> Reload project &#xff0c;还是不生效&#xff0c;但是同事&#xff08;别人&#xff09;那里正常。 问题出现的环境&#xff1a; 可能项目是多模块项目&#xff0c;结构…

postman测试导入文件

01 上传文件参数 1.选择请求方式 选择post请求方式&#xff0c;输入请求地址 2.填写Headers Key&#xff1a;Content-Type &#xff1b; Value&#xff1a;multipart/form-data 如下图 3.填写body 选择form-data&#xff0c;key选择file类型后value会出现按钮&#xff0…

2023.1.17 关于 Redis 持久化 AOF 策略详解

目录 引言 AOF 策略 实例演示一 缓冲区 重写机制 手动触发 自动触发 AOF 重写流程 实例演示二 引言 Redis 实现持久化的两大策略 RDB ——> Redis DataBase&#xff08;定期备份&#xff09;AOF ——> Append Only File&#xff08;实时备份&#xff09; 注意&…

Operation

contents 服务器一、相关概念1.1 云服务器与实例1.2 关于域名解析延时与80端口1.3 关于备案1.4 关于SSL证书1.5 关于SSL证书的签发1.6 关于SSL证书的部署1.7 关于LNMP和LAMP1.8 关于bt面板 二、单服务器单一级域名多网站2.1 创建多个二级域名2.2 解析二级域名绑定到服务器上2.3…

洛谷 P1126 机器人搬重物

题目描述 机器人移动学会&#xff08;RMI&#xff09;现在正尝试用机器人搬运物品。机器人的形状是一个直径 1.6 米的球。在试验阶段&#xff0c;机器人被用于在一个储藏室中搬运货物。储藏室是一个 NM 的网格&#xff0c;有些格子为不可移动的障碍。机器人的中心总是在格点上…

数仓建设学习路线(三)元数据管理

什么是元数据&#xff1f; 简单来说就是描述数据的数据&#xff0c;更直白来说就是描述表名、表制作者、表字段、表生命周期、表存粗等信息的数据 元数据该如何管理 工具化 开源&#xff1a; 可通过atlas获取表依赖及信息做二次开发&#xff0c;或者完成可视化界面 平台化&am…

为什么单片机不能直接驱动继电器和电磁阀?

为什么单片机不能直接驱动继电器和电磁阀&#xff1f; 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&…

FastDFS分布式文件存储

为什么会有分布式文件系统&#xff1f; 分布式文件系统是面对互联网的需求而产生。因为互联网时代要对海量数据进行存储。很显然靠简单的增加硬盘个数已经满足不了我们的要求。因为硬盘传输速度有限但是数据在急剧增长&#xff0c;另外我们还要要做好数据备份、数据安全等。采用…

【linux】Debian防火墙

Debian系统默认没有安装防火墙&#xff0c;但用户可以根据需要自行选择并安装一个防火墙以增强系统安全性。 一、查看Debian 桌面系统的防火墙是否关闭 在Debian及其他基于Linux的桌面系统中&#xff0c;防火墙功能通常是由iptables或nftables规则集控制的&#xff0c;而ufw&…

pikachu验证码绕过第三关攻略

打开pikachu靶场第三关&#xff1a; 挂上代理&#xff0c;随便输入账户密码&#xff1a; 返回bp。进行放包发现显示token错误。 每一次登录的返回包会带有token相关数据用于下一次的登录认证&#xff1a; 进行替换token值&#xff1a; 替换完成开始进行检点的爆破&#xff1a;…

【Python时序预测系列】基于Holt-Winters方法实现单变量时间序列预测(源码)

一、引言 Holt-Winters是一种经典的时序序列预测方法&#xff0c;用于对具有季节性和趋势性的数据进行预测。在这种方法中&#xff0c;使用三个组件来建模时序数据&#xff1a;趋势&#xff08;Trend&#xff09;、季节性&#xff08;Seasonality&#xff09;和残差&#xff0…

点亮流水灯

目录 1.water_led 2.tb_water_led 50MHZ一个周期是20ns,0.5秒就是20ns0.02um0.00002ms0.000_00002s。0.5/0.000_00002s25_000_000个时钟周期&#xff0c;表示要从0计数到24_999_999 LED灯是低电平点亮&#xff0c;前0.5秒点亮第一个LED灯&#xff0c;当检测到脉冲信号点亮第二…

Flutter 滚动布局:sliver模型

一、滚动布局 Flutter中可滚动布局基本都来自Sliver模型&#xff0c;原理和安卓传统UI的ListView、RecyclerView类似&#xff0c;滚动布局里面的每个子组件的样式往往是相同的&#xff0c;由于组件占用内存较大&#xff0c;所以在内存上我们可以缓存有限个组件&#xff0c;滚动…