HashMap实现原理:容量、负载因子、hash与定位你都搞定了吗?
mhr18 2024-09-29 14:16 26 浏览 0 评论
HashMap是常考点,而一般不问List的几个实现类(偏简单)。以下基于JDK1.8.0_102分析。
JDK版本:oracle java 1.8.0_102
内部存储
HashMap的内部存储是一个数组(bucket),数组的元素Node实现了是Map.Entry接口(hash, key, value, next),next非空时指向定位相同的另一个Entry,如图:
容量(capacity)和负载因子(loadFactor)
简单的说,capacity就是bucket的大小,loadFactor就是bucket填满程度的最大比例。当bucket中的entries的数目(而不是已占用的位置数)大于capacity*loadFactor时就需要扩容,调整bucket的大小为当前的2倍。同时,初始化容量的大小也是2的次幂(大于等于设定容量的最小次幂),则bucket的大小在扩容前后都将是2的次幂(非常重要,resize时能带来极大便利)。
Tips:
默认的capacity为16,loadFactor为0.75,但如果需要优化的话,要考量具体的使用场景。
- 如果对迭代性能要求高,不要把capacity设置过大,也不要把loadFactor设置过小,否则会导致bucket中的空位置过多,浪费性能
- 如果对随机访问的性能要求很高的话,不要把loadFactor设置的过大,否则会导致访问时频繁碰撞,时间复杂度向O(n)退化
- 如果数据增长很快的话,或数据规模可预知,可以在创建HashMap时主动设置capacity
hash与定位
作为API的设计者,不能假定用户实现了良好的hashCode方法,所以通常会对hashCode再计算一次hash:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashCode方法
注意key.hashCode()的多态用法。重点是hash方法。
hash方法的实现和定位
前面已经说过,在get和put计算下标时,先对hashCode进行hash操作,然后再通过hash值进一步计算下标,如下图所示:
回顾hash方法的源码可知,hash方法大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或。
javadoc这样说:
Computes key.hashCode() and spreads (XORs) higher bits of hash to lower. Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.) So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don’t benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.
在设计hash函数时,因为目前的table长度n为2的次幂,所以计算下标的时候,可使用按位与&代替取模%:
(n - 1) & hash
设计者认为这方法很容易发生碰撞。为什么这么说呢?不妨思考一下,在n - 1为15(0x1111)时,散列真正生效的只是低4bit的有效位,当然容易碰撞了。
因此,设计者想了一个顾全大局的方法(综合考虑了速度、作用、质量),就是把高16bit和低16bit异或了一下。设计者还解释到因为现在大多数的hashCode的分布已经很不错了,就算是发生了碰撞也用O(logn)的tree去做了。仅仅异或一下,既减少了系统的开销,也不会造成因为高位没有参与下标的计算(table长度比较小)时,引起的碰撞。
好吧,我还是不理解为什么“很”容易发生碰撞。难道hash的分布不是均匀的吗?
碰撞
调用put方法时,尽管我们设法避免碰撞以提高HashMap的性能,还是可能发生碰撞。据说碰撞率还挺高,平均加载率到10%时就会开始碰撞。我们使用拉链法来处理碰撞节点。
将旧entry的引用赋值给新entry的next属性,改将新entry放在该位置——即在该位置上存储一个链表,冲突节点从链表头部插入,这样插入新entry时不需要遍历链表,时间复杂度为O(1)。但如果链表过长,查询性能仍将退化到O(n)。Java8中对链表长度增加了一个阈值,超过阈值链表将转化为红黑树,查询时间复杂度降为O(logn),提高了链表过长时的性能。
勘误:网上有朋友联系我指出了此处的错误,Java8中,发生碰撞时会遍历到链表的最后。
这篇文章写的时候读源码能力比较渣,就基于自己早期对Java7源码的分析改编,本以为除了红黑树部分,大体相同,就直接copy了过来,导致了这个错误——当然,现在想来,关于Java7中HashMap的分析也是错的,最后插入方式没错,但是插入前还是要O(n)的时间遍历一遍。不过其他涉及源码的文章就都是老老实实追源码写出来的,可以放心阅读。告诫自己,要坚持踏实,坚持怀疑。以下是改正的分析。
从该位置上的entry开始,遍历到找到相同的key(就替换),或到结尾还是没有相同的key(就衔接在尾节点的next上)。查询时过程相同。不考虑任何优化,插入、查询的性能都是O(n)。Java8中对链表长度增加了一个阈值,超过阈值链表将转化为红黑树,插入、查询的时间复杂度降为O(logn),提高了链表过长时的性能。调用get方法时,定位到该位置,再遍历红黑树,比较key值找到所需元素:
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;
}
判断元素相等的设计比较经典,利用了bool表达式的短路特性:先比较hash值;如果hash值相等,就通过==比较;如果==不等,再通过equals方法比较。hash是提前计算好的;==直接比较引用值;equals方法最有可能耗费性能,如String的equals方法需要O(n)的时间,n是字符串长度。一定要记住这里的判断顺序,很能考察对碰撞处理源码的理解。
针对HashMap的使用,此处要注意覆写hashCode和equals方法时的两个重点:
- 覆写后,一定要保证equals判断相等的时候,hashCode的返回值也相等。
- 对于选作key的类,要保证调用put与get时hashCode的返回值相等,equals的性质相同。
resize
resize是HashMap中最难理解的部分。
调用put方法时,如果发现目前的bucket占用程度已经超过了loadFactor,就会发生resize。简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中。
javadoc中这样说:
Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.
即,当超过限制的时候会resize,又因为我们使用的是2次幂的扩展,所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
怎么理解呢?例如我们从16扩展为32时,具体的变化如下:
假设bucket大小n=2^k,元素在重新计算hash之后,因为n变为2倍,那么新的位置就是(2^(k+1)-1)&hash。而2^(k+1)-1=2^k+2^k-1,相当于2^k-1的mask范围在高位多1bit(红色)(再次提醒,原来的长度n也是2的次幂),这1bit非1即0。如图:
所以,我们在resize的时候,不需要重新定位,只需要看看原来的hash值在新增bit的位置上是1还是0就好了,是0的话位置没变,是1的话位置变成“原位置+oldCap”。代码比较长就不贴了,下面为16扩充为32的resize示意图:
这个设计非常的巧妙,如果认为hash值是随机的,那么新增的1bit是0还是1也是随机的,因此resize的过程随机的把之前的冲突的节点分散到新的bucket中了。
关注我:转发+私信回复“架构资料”获取往期Java高级架构资料、源码、笔记、视频
Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、
高并发等架构技术
最后的话
为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀。而你是不是还在满足于现状且内心在窃喜?“对于程序员来说,如果哪一天开始他停止了学习,那么他的职业生涯便开始宣告消亡。”所以行动起来,学习起来!
相关推荐
- jdk17安装和配置环境变量(jdk安装步骤环境变量配置)
-
简单介绍JDK17是Java的一个长期支持(LTS)版本,于2021年9月发布。作为LTS版本,它将获得Oracle的长期支持,这意味着会获得安全更新和错误修复,直到2...
- jdk8安装和配置环境变量(jdk8配置环境变量win10)
-
简单介绍JDK8是Java开发工具包的一个重大版本,于2014年3月发布。它引入了许多重要的新特性,极大地改进了Java编程的体验,并提高了代码的效率和可读性。总结JDK8是一...
- Java开发环境的准备流程!(java开发环境的准备流程有哪些)
-
准备开发环境工欲善其事,必先利其器。在学习本书的技术内容之前,应先将开发环境搭建好。本书所涉及的基础环境将在本章准备,包括JDK、IntelliJIDEA和Maven。如果读者对这些环境较为熟悉,可...
- skywalking-全链路跟踪(链路追踪sleuth)
-
一、概述1、skywalking简介SkyWalking是一款广受欢迎的国产APM(ApplicationPerformanceMonitoring,应用性能监控)产品,主要针对微服务、Clou...
- Oracle项目管理系统之成本状态跟踪
-
卷首语成本费用管理模块功能分为公司级、项目级两个层级,其中公司级功能包括概预算管理、概预算定额管理、预算模板管理,以及控制估算管理等功能。费用与成本成本费用管理主要是为了合理利用资金资源,控制企业的成...
- 从入门到精通,Java最全学习路线(java自学路线)
-
最近好多小伙伴询问,该如何学习java?那些视频适合零基础学习呢?大多数都是一些刚入门的新手,还不了解这个行业,也不知道从何学起,开始的时候非常迷茫,在这给大家整理出一些学习路线。Java学习路线在这...
- SPARC M7最新技术 甲骨文首度在台发表
-
甲骨文首度在台发表SPARCM7最新技术,宣布推出全新SPARC系统產品系列,採用革命性的32内核、256执行绪SPARCM7微处理器。这套SPARC系统提供:先进的入侵防护和加密的Securit...
- 轻量化的企业级OLAP方案2(轻量化ar)
-
上一篇文章讲了一个轻量化的企业级OLAP方案,这一篇文章继续介绍另外一个方案。方案2:Greenplum1Greenplum介绍1.1Greenplum概念Greenplum数据库是一种基于Po...
- 时序数据库在水电站领域的应用(时序数据库优缺点)
-
大家好,上期经过了王宏志教授对数据库前沿领域的介绍,不知道小伙伴们是否对数据库领域有了更深的认识呢,本期让我们回归时序数据库,再来聊一聊时序数据库在水电站领域的应用。本文仅代表个人观点,如有偏颇之处,...
- 数据库SQL引擎比较(数据库引擎有哪些,有什么区别)
-
数据库SQL引擎的典型工作机制及详细分析如下,涵盖查询处理全流程中的核心机制与实际案例:一、SQL引擎核心处理流程1.解析与校验阶段词法/语法解析:SQL语句被拆解为Token序列,构建...
- Ubuntu系统密码(root密码)的修改方法
-
本文介绍在Linux系统的Ubuntu电脑中,修改账户用户密码(同时也修改了root用户密码)的方法。首先,如果此时处于登录页面(也就是意识到自己忘记密码的那个页面),就先点击右上角的关闭按钮...
- TablePlus:一个跨平台的数据库管理工具
-
TablePlus是一款现代化的跨平台(Window、Linux、macOS、iOS)数据库管理工具,提供直观的界面和强大的功能,可以帮助用户轻松管理和操作数据库。TablePlus免费版可以永久...
- 云满足用户对通过单一视图了解整体运营和数据的需求
-
ZD至顶网软件频道消息:云在简化IT和业务的同时也带来了新问题,这很有趣,不是吗?毫无疑问,云大幅降低了企业运行基础设施和应用的复杂性和成本;但您仍然面临一个老生常谈的问题——孤岛,只不过这次是在云...
- SQL_TOP(显示数据条数)(sqlserver显示查询的行数)
-
(8)SELECT(9)DISTINCT(11)<topNum><selectlist>(1)FROM[left_table](3)<join_type>...
- php搭建小型部门网站(php构建网站)
-
熟悉前端html,js,css,后端熟悉php,python,java只能称得上一般,还有很多坑待踩。鉴于所学很杂,决定利用对业务的熟悉来搞一个部门网站来简化大家的工作流程。初步设计方案后端用p...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle 空为0 (51)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)