HashMap难点分析

HashMap简介

HashMap通过数组+链表(+红黑树)组成

HashMap重要变量介绍

//HashMap初始容量 计算结果16,如果手动指定HashMap的初始容量,则应为2的次方数,即2ⁿ
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
//负载因子 默认0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;     
//扩容阈值 默认 以上两数相乘 即 16 * 0.75 = 12,桶内元素个数>=该值时 发生扩容
int threshold;       
//链表长度阈值
static final int TREEIFY_THRESHOLD = 8;
//链表树化最小容量 即当链表长度超过8时并hashMap容量已达到64 则会将链表转换为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;

hash方法 - key的hash值计算

//在put时会调用putval方法 传参时计算key值的哈希值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  1. HashMap允许仅存储一个key为null的键值对,当key为null时,默认hash值为0,即对应桶下标为0的位置
  2. 当key不为null时,key值的哈希值计算方式为
(h = key.hashCode()) ^ (h >>> 16)

解释一下:
key.hashCode():
方法返回int类型的key值内存地址的散列值,并将该值赋予h变量。
众所周知,一个int类型占用4个字节,即32位二进制数。
假设当前的key为: "hello world",则对应的hashCode值为:0110 1010 1110 1111 1110 0010 1100 0100

h >>> 16:
将h值无符号右移16位,高位补0,则得到的结果即key的高16位数:0000 0000 0000 0000 0110 1010 1110 1111

^:
按位异或,相同为0,不同为1

则当前代码的按位异或计算结果为:
  0110 1010 1110 1111 1110 0010 1100 0100
^ 0000 0000 0000 0000 0110 1010 1110 1111
--------------------------------------------
  0110 1010 1110 1111 1000 1000 0010 1011

当然,到这里可能还不能理解为什么需要这样做,继续往下看。

如果你看过HashMap的put方法源码,你会发现元素寻址(即在桶中的下标)的计算方法是:

//n表示HashMap容量
(n - 1) & hash

可能你有听说过在HashMap中,元素下标的计算方式是 数组长度 % 当前元素的hash值(即取余操作) 得到的。
嗯,以上计算方法即等同于%取余操作,只是系统并不认识取余操作,所有的操作都是加法操作,这样写会提升效率。

&:
按位与,同一位均为1,则结果为1,否则为0。如:
  1001
& 1100
-----------
  1000

回归正题,为什么要将key的hashCode值与其高16位进行按位异或计算?
文章开头介绍过,hashMap的容量应为2的次方数,假设当前hashMap的容量值为初始容量16,即2⁴。
如果我们单单把key的hashCode和n-12-1=15,二进制为1111)进行按位与计算:
  0110 1010 1110 1111 1110 0010 1100 0100
& 0000 0000 0000 0000 0000 0000 0000 1111
-------------------------------------------
  0000 0000 0000 0000 0000 0000 0000 0100
  
可以发现,hashCode值只有低4位起到了作用,高位全部失效,假设我们put的key值高位不同,低4位均为0100,则会发生严重的hash冲突,导致链化严重影响get性能。且通常情况下,HashMap的容量并不会特别大,所以通过n-1得到的二进制数有效位非常有限,通常在16位以内,这样会导致key的Hashcode16位浪费。
而通过key的HashCode值与其高16位数进行按位异或后得到的hash值,再与1111进行按位与计算,会大大降低hash冲突几率,均匀散列。

put方法

put方法存在4种情况:

  1. 未初始化或当前桶不存在元素,当前hashMap中的数组Node<K,V>[] table未初始化,则会调用resize()进行初始化,初始化完成后将当前元素封装成Node对象根据hash值计算的下标插入table即可,或当前table已经初始化了,但是经hash值计算出来的下标并未发生hash冲突,则直接插入下标位置即可。

ps: HashMap的数组是懒加载模式,只有在第一次put时才会初始化table。

  1. 发生hash冲突,但链表还未链化时,根据key的hash值和key本身比较下标位置的Node元素,若相同则覆盖,否则根据尾插法生成链表,在尾部插入当前元素即可。

  2. 发生hash冲突,且链表已链化时,使用迭代器遍历当前数组下标位置对应的链表,并根据key的hash值和key本身逐一对比链表中的Node对象元素对应的字段值,若存在重复,则使用新的value值直接覆盖当前元素的value值,否则在链表尾部插入新的Node对象。并检查链表长度是否到达树化长度,如果达到了则检查当前HashMap容量是否到达64(默认),若未达到则进行扩容,否则将链表转换为红黑树。

  3. hash冲突严重,链表转为红黑树。(待后续文章解析)

resize() - 扩容机制

  • 发生条件:
  1. 当链表长度大于8,且HashMap容量小于64时,发生扩容。
  2. 当桶个数大于扩容阈值threshold时,发生扩容。

当触发扩容时,HashMap会对原来的容量进行左移1位运算,即oldCap*2的操作,使新容量为原来的两倍。

数据迁移:

当初始化好新容量数组后,会对老数组进行遍历并进行数据迁移。

数据迁移分为4种情况:

  1. 当前桶不存在元素, 直接跳过不处理;
  2. 当前桶仅存在一个元素,即未发生哈希冲突,不存在链表。直接将该Node元素的hash值与新数组的长度-1进行按位与运算,计算出新下标后直接插入。
  3. 当前桶存在链表,即发生了哈希冲突。此时HashMap会将该链表分为高位链低位链
高位链、低位链的解释:
在旧数组中,同处同一个桶的元素的低位一定相同的。
如,旧容量为8,则在旧数组中下标为1的桶中的元素低4位一定是0001,因为只有当元素的低4位hash值为0001时,在与旧数组长度-1(二进制为1111)与0001进行按位与计算(即 n - 1 & hash)时,才会计算出下标为1的数值。
  0001
& 1111
--------
  0001
  
而在1号桶中的所有元素的hash值虽然低4位都相同,但是高位(即第5位)可能是1,也可能是0,即存在两种可能性。
所以在发生扩容时,会进行判断 (e.hash & oldCap) == 0,得出当前桶数据迁移所对应的下标。
请注意,这里并没有使用newCap - 115的二进制数0001 1111), 而是使用oldCap(8的二进制0001 0000.原因是,
我们说过HashMap的容量是2的次方数,这种数字有一个特点就是-1后的值是 n个1,如8 - 1 = 111116 - 1 = 1 1111
而按位与的计算方式是,同一位为1则结果为1,否则为0,
而低4位跟1111在扩容前已经计算过了,只需要再计算一下高位(第5位)的按位与结果是0还是1就可以知道当前元素的桶下标。
那么就存在两种情况,
1. (e.hash & oldCap) == 0成立,即当前元素高位为0,则当前元素被归类到低位链,保持当前元素位置不变,即继续存储在1号桶中
2. (e.hash & oldCap) == 0不成立,那就是返回值为1,当前元素高位为1,则当前元素被归类到高位链,下标值为j + oldCap(即老表的位置+老表的长度计算得出的下标值,本例中为1 + 8 = 9)的位置进行存储。
  1. 当前桶存在红黑树。处理方式与第3点大概相同,也是将元素拆分为高位链和低位链。TreeNode仍然保留了Node中的next字段,也就是说红黑树内部结构中仍然维护着一个链表,只是查询的时候并不会使用到它,但是在新增节点或者删除节点的时候仍然需要维护这个链表。这个next指针构造的链表结构也就是为了方便在去构建高位链和低位链时使用。不同的是,在拆分出来高位链和低位链后,会判断高位链链表长度是否小于等于6,若成立,则将treeNode转为普通Node链表,否则需要重建红黑树并放入到新数组中。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值