HashMap原理解析

2021-10-30  本文已影响0人  嗷嗷待哺丶

一、hashmap的数据结构

底层数组+单向链表,jdk1.8后,链表长度大于8,并且数组长度大于64,将转为用红黑树存储

二、hashmap用了hash为什么还要equals

因为hash也会重复,hash重复但是值不一定相等,所以要使用equals进行判断

三、hashmap中put方法的过程

  1. 计算hash值, key.hashCode()>>>16,用高16位和低16位进行异或运算(二进制按位比较,如果相同则为 0,不同则为 1)。
  2. 然后根据hash值计算出数组下标(跟数组长度进行与运算),将元素放入数组中,如果没有出现哈希冲突,则直接放入数组,如果出现哈希冲突,则以链表的方式放在链表最后(jdk1.7使用头插法,也就是放在链表最前面,jdk1.8使用了尾插法,放在了链表最后面)。
  3. 如果链表的长度超过8,并且数组长度大于64,就把链表转成红黑树,如果链表长度低于6,就把红黑树转回链表;
  4. 如果数组中键值对的总量超过了阈值(负载因子0.75),会进行动态扩容。

四、负载因子是什么

负载因子用来实现hashmap动态扩容,默认0.75是在时间和空间上折中的一个选择。

扩容阈值计算公式:threshold = capacity * loadFactor (capacity是数组容量,初始化默认16,loadFactor是负载因子,初始化默认0.75)

负载因子对hashmap的影响

默认的负载因子 DEFAULT_LOAD_FACTOR = 0.75,这是 jdk 设置的一个比较合适的负载因子,一般情况下不推荐去修改它。特殊情况下:

五、hash冲突有什么解决方式

六、hashmap为什么在jdk1.8后引入了红黑树?为什么不一直使用?

之所以引入红黑树是为了解决二分查找树的缺陷,二分查找树在特殊情况下会变成一条线性结构(这就跟原来的链表结构一样了,造成很深的问题),遍历查询会非常慢。

而红黑树在插入新数据后会通过左旋、右旋、变色这些操作来保持平衡,引入红黑树就是为了查询数据快,解决链表查询深度的问题,红黑树属于平衡二叉树,但是为了保持“平衡”是要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

七、为什么hashmap的容量必须是2的n次幂

数组大小必须为 2 的 n 次幂,也就是 16、32、64….不能为其他值。因为如果不是 2 的 n 次幂,那么经过计算的数组下标会增大碰撞的几率。

如果hash值的二进制是:
10000(十进制16)
10001(十进制17)
10010(十进制18)

和10100做&运算后(与运算,二进制按位比较,都为1结果才为1,否则为0),结果都是10000,也就是都被映射到数组16这个下标上。这三个值会以链表的形式存储在数组16下标的位置。这显然不是我们想要的结果。

但如果数组长度n为2的n次幂,2进制的数值为10,100,1000,10000……n-1后对应二进制为1,11,111,1111……这样和hash值低位&后,会保留原来hash值的低位数值,那么只要hash值的低位不一样,就不会发生碰撞。

同时 (n - 1) & hash 等价于 hash%n,那么为什么不直接用hash%n呢?这是因为按位的操作效率会更高。

1. 为什么不直接用 key 的 hashCode?

其实说到底还是为了减少碰撞的概率。我们先看看 计算hash值方法中的代码做了什么事情:

h ^ (h >>> 16)

这个意思是把 h 的二进制数值向右移动 16 位。我们知道整形为 32 位,那么右移 16 位后,就是把高 16 位移到了低 16 位。而高 16 位清0了。

^为异或操作,二进制按位比较,如果相同则为 0,不同则为 1。
这行代码的意思就是把高低16位做异或。如果两个hashCode值的低16位相同,但是高位不同,经过如此计算,低16位会变得不一样了。

2. 为什么要把低位变得不一样呢?

这是由于哈希表数组长度n会是偏小的数值,那么进行 (n - 1) & hash 运算时,一直使用的是hash较低位的值。那么即使hash值不同,但如果低位相当,也会发生碰撞。而进行 h ^ (h >>> 16) 加工后的hash值,让hashCode高位的值也参与了哈希运算,因此减少了碰撞的概率。

八、红黑树特点

红黑树是一种近似平衡的二叉查找树,他并非绝对平衡,但是可以保证任何一个节点的左右子树的高度差不会超过二者中较低的那个的一倍。

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点必须是黑色。叶子节点必须是黑色NULL节点。
  3. 红色节点不能连续。
  4. 对于每个节点,从该点至叶子节点的任何路径,都含有相同个数的黑色节点。
  5. 能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决。

九、hashmap扩容机制

  1. 如果table == null, 则为HashMap的初始化, 生成空table返回即可;
  2. 如果table不为空, 需要重新计算table的长度, newLength = oldLength << 1(注, 如果原oldLength已经到了上限, 则newLength = oldLength);
  3. 遍历oldTable:
    1. 首节点为空, 本次循环结束;
    2. 无后续节点, 重新计算hash位, 本次循环结束;
    3. 当前是红黑树, 走红黑树的重定位;
    4. 当前是链表, JAVA7时还需要重新计算hash位, 但是JAVA8做了优化, 通过 (e.hash & oldCap) == 0 来判断是否需要移位; 如果为真则在原位不动, 否则则需要移动到当前 hash槽位 + oldCap 的位置;

再来看下 e.hash & oldCap == 0为什么可以判断当前节点是否需要移位, 而不是再次计算hash;

部分源码:

//把原链表中的元素插入到 low 和 high 链表中,依靠 (e.hash & oldCap) == 0 判断
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
    if (loTail == null)
        loHead = e;
    else
        loTail.next = e;
    loTail = e;
}
else {
    if (hiTail == null)
        hiHead = e;
    else
        hiTail.next = e;
    hiTail = e;
}


//把 low 的头结点插入到当前数组下标的位置,
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
//把 high 的头结点插入  新数组中 [当前数组下标 + 旧数组长度] 的位置
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}
上一篇下一篇

猜你喜欢

热点阅读