image

编辑人: 青衫烟雨

calendar2025-02-08

message9

visits1112

为什么HashMap数组的长度必须是2的指数次幂

分析&回答

先看源码如何计算Hash值,

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

计算机的运行效率:加法(减法)>乘法>除法>取模


数组一旦达到容量的阈值的时候就需要对数组进行扩容。那么扩容就意味着要进行数组的移动,数组一旦移动,每移动一次就要重回记算索引,这个过程中牵扯大量元素的迁移,就会大大影响效率。那么如果说我们直接使用与运算,这个效率是远远高于取模运算的。

// 看看putVal 方法使用与运算
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
    
tab[i = (n - 1) & hash]中tab就是HashMap的实体数组,
其下标通过i = (n - 1) & hash来获取(n表示数组长度,hash表示hashCode值),
1、与运算(n-1) & hash
2、取代取模运算hash%length
两种方式记算出来的结果是一致的(n就是length),也就是(length-1)&hash = hash%length

当数组的长=长度为2的指数次幂时,例如:假设数组长度为4,哈希值为10
(n-1) & hash = (4-1) & 10 = 00000011 & 00001010 = 00000010 = 2
hash % length = 10 % 4 = 2
即length-1)&hash = hash&length

但是当数组的长=长度不为2的指数次幂时,例如:再假设数组长度为5,哈希值10
(n-1) & hash = (5-1) & 10 = 00000100 & 00001010 = 00000000 = 0 
hash % length = 10 % 5 = 2
即length-1)&hash ≠ hash&length

通过上面看出,这必须保证数组长度为2的整数次幂。

综上所述使用位运算来加快计算的效率,只有当数组长度为2的指数次幂时,其计算得出的值才能和取模算法的值相等,并且保证能取到数组的每一位,减少哈希碰撞,不浪费大量的数组资源。

反思&扩展

为什么要无符号右移16位后做异或运算?

image-1691384913474

将h无符号右移16为相当于将高区16位移动到了低区的16位,再与原hashcode做异或运算,可以将高低位二进制特征混合起来

从上文可知高区的16位与原hashcode相比没有发生变化,低区的16位发生了变化

我们可知通过上面(h = key.hashCode()) ^ (h >>> 16)进行运算可以把高区与低区的二进制特征混合到低区,那么为什么要这么做呢?

我们都知道重新计算出的新哈希值在后面将会参与hashmap中数组槽位的计算,计算公式:(n - 1) & hash,假如这时数组槽位有16个,则槽位计算如下:

image-1691384927762

仔细观察上文不难发现,高区的16位很有可能会被数组槽位数的二进制码锁屏蔽,如果我们不做刚才移位异或运算,那么在计算槽位时将丢失高区特征

也许你可能会说,即使丢失了高区特征不同hashcode也可以计算出不同的槽位来,但是细想当两个哈希码很接近时,那么这高区的一点点差异就可能导致一次哈希碰撞,所以这也是将性能做到极致的一种体现。


喵呜面试助手: 一站式解决面试问题,你可以搜索微信小程序 [喵呜面试助手] 或关注 [喵呜刷题] -> 面试助手 免费刷题。如有好的面试知识或技巧期待您的共享!

创作类型:
原创

本文链接:为什么HashMap数组的长度必须是2的指数次幂

版权声明:本站点所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明文章出处。
分享文章
share