首页IT科技java中hashmap实现原理(java中HashMap的设计精妙在哪?)

java中hashmap实现原理(java中HashMap的设计精妙在哪?)

时间2025-08-05 04:02:20分类IT科技浏览4434
导读:摘要:本文结合图解和问题,教你一次性搞定HashMap...

摘要:本文结合图解和问题              ,教你一次性搞定HashMap

本文分享自华为云社区《java中HashMap的设计精妙在哪?用图解和几个问题教你一次性搞定HashMap》                     ,作者:breakDawn              。

HashMap核心原理

HashMap完整的put过程

以下是对上图的详细解释:

首先      ,要获取key的哈希值                     。

如果为空          ,就统一是0

否则                      ,调用对象的.hashCode()方法         ,接着再与自己的右移16位进行异或      ,以便充分利用高位信息      。 接着判断内部node数组是否为空                      ,如果是            ,先进行初始化扩容          。默认为16                      。 根据(n-1)&hash值   ,获取哈希表索引位置         。 哈希表的node数组中                      ,存放的是每组链表的头节点      。

先检查头节点是否和自己要存放的key完全匹配 (hash值相同                ,key值相同,先hash再key                  ,是因为hash的判断简单                    ,key的equals判断可能会复杂)                      。如果匹配   ,得到需要替换的节点            。 头节点和自己要放的key不匹配              ,则判断一下这个头节点是否是红黑树节点                     ,如果是      ,说明已经升级成红黑树了          ,调用putTree插入到红黑树中   。 如果不是红黑树                      , 那就是遍历链表         ,完全匹配就得到需要替换的节点                      。如果到尾部了      ,也没匹配的                      ,则插入新节点                。 如果前面找到了要替换的节点            ,则判断一下是否可以替换(是否没要求putIfAbsent   ,或者value为null)                      ,是就替换                ,不是就结束 如果前面是插入了新节点,非替换                  , 则要modCount++(方便迭代器确认map是否更新)                    , 同时++size   , 然后和扩容阈值做判断              , 如果太大                     ,就resize进行扩容

hashMap的扩容过程      ,java7和8扩容的区别

java7:

当resize时          ,新建一个数组newTable 遍历原table中的每个链表和节点                      ,重新hash         ,找到新的位置放入 放入的方式是头插法      ,即始终插在链表的头节点。

java8:

不再每个点rehash放置                      ,而是最高位是0则坐标不变            ,最高位是1则坐标变为“10000+原坐标              ”   ,即“原长度+原坐标. 避免了频繁的哈希计算和搬移过程                  。 使用尾插法在链表上插入节点 桶内元素超过8个                      ,链表转成红黑树

为什么java8要改成尾插法?

A:多线程时                ,java7的map-put可能造成死循环                    。

A线程扩容到那一半, 还处在遍历链表做头插法搬移的过程时                  ,存了2个局部变量                    ,当前链点now指向a   , next指向b              ,正准备搬移(a->b->c这样的链表                     ,a是头节点)

B线程则同时完成线程扩容      ,但是map里都是引用          ,浅拷贝                      ,** 因为是头插法         , 会导致顺序变化**      , 原本a->b->c 变成了c->b->a   。

因此A恢复时                      , 链点还是a            ,next还是b   , 于是往下走到了b                      , 取bbs的next时                ,已经变成了a, 于是发生了a->b->a的循环

导致后续操作的next都是错误操作                  ,引发环形指针              。

java8里改成尾插法                    ,这样做resize时   ,a->b->c 如果仍然哈希到同一个节点              , 顺序是不会发生变化的                     。

虽然解决了死循环问题                     , 但java8的hashMap仍然是线程不安全的      ,为什么?

A:因为缺乏同步          ,导致同节点发生哈希碰撞时                      ,if条件的判断都可能是有问题的         ,导致本该插在链表头节点后面的      ,结果直接作为链表头覆盖到数组上了      。

具体到底满足什么情况                      ,才会resize扩容呢?

A:HashMap负载因子 LoadFactor            ,默认值为0.75f          。

衡量HashMap是否进行Resize的条件如下:

HashMap.Size >= Capacity * LoadFactor

另一种情况                      。JDK1.8源码中   ,执行树形化之前                      ,会先检查数组长度                ,如果长度小于64,则对数组进行扩容                  ,而不是进行树形化

扩容后                    ,capacity扩容多少倍呢?为什么

A:哈希表每次扩容是两倍         。

初始长度为2的幂次方   ,随后以2倍扩容的方式扩容              ,元素在新表中的位置要么不动                     ,要么有规律的出现在新表中(二的幂次方偏移量)      ,这样会使扩容的效率大大提高      。

另外          ,hashmap采用二倍扩容还有另外一个好处:可以使元素均匀的散布hashmap中                      ,减少hash碰撞                      。

点击关注         ,第一时间了解华为云新鲜技术~

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
javascript入门教程(JavaScript 入门基础 / 概念介绍(一)) vue系统通知(vue通知提醒消息)