关于HashMap,90%的人都不知道的点!

提示:本篇文章为上篇文章的延续,需要理解上篇文章内容才能对本篇内容进行理解。

前篇回顾: 上篇文章我们讲解了

hash函数作用、hash值的计算、hash碰撞的解决。

文章: 细粒度拆分HashMap<2>

以及分析了为什么要用">>16",因为hash表是使用2的幂次方倍(也就是说HashMap数组长度取2的整数幂)进行掩码(也就是代码中的异或"^")的,在掩码的变化过程中,hash散列可能总发生相撞,所以采用高位扩散进行向下的变换(自己的高半区和低半区做异或)来避免一些特殊情况的发生,什么特殊情况呢?

HashMap通过hashCode计算,然后对数组下标进行寻址,这个时候可能会发生冲突(hash碰撞或说成相撞)。

高16位与低16位异或来减少这种碰撞影响,源码中的”>>>“正是用高16位的方式进行处理干扰。
在这里插入图片描述

可能有人又有疑问,这样就行了?

没错,这样不仅能够混淆原始哈希码(h)的低位才加大低位的随机性,且低位还能掺杂高位的部分特性,将高位的信息变相保留。

对于这种解决方法也只是尽量减少hash的碰撞,达到干扰hash碰撞的效果,但是hash碰撞还是无法避免!

好了对于上面的问题分析就此结束,可以说该问题偏向于数学的范畴,在HashMap源码中也多次出现此类数学性问题。

例如:

在关于 “为什么大多时候hashCode要选择使用31作为生成hashCode的乘数” 这个问题给出的答案:

在名著 《Effective Java》第 42 页就有对 hashCode 为什么采用 31 做了说明:

之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能:31 * i == (i << 5) - i。

可能读者还是没有搞太清楚:

我们知道在HashMap中底层计算hashCode是通过调用Arrays.hashCode()函数进行计算的:

public static int hashCode(Object a[]) {
    if (a == null){
        return 0;
    }
    int result = 1;
    for (Object element : a){
        result = 31 * result + (element == null ? 0 : element.hashCode());
    }
    return result;
}

为什么要采用31这个质数?

第一个原因就是更多的减少乘积结果造成的冲突:

选择质数2,计算得出的乘积结果范围又太小,选择超过100的质数,计算得出的结果又超过int的最大范围,国外做过测试,对超过50000个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),那么这几个数就被作为生成hashCode值得备选乘数了。

第二个原因就是31能够被JVM给优化:

也就是这个公式:31*i == (i<<5)-1,JVM最有效的计算方式就是位运算。

结合两条结论固得出”为什么大多时候hashCode要选择使用31作为生成hashCode的乘数“的答案。

但是此时又有新的疑问带给我们:”为什么HashMap容量要取2的整数幂呢?“

我们可以先来看看源码中的这一个方法:

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

让我们来看看该方法内部如何实现的:首先将容量减一,接着进行五次无符号右移操作,且第五次右移、key的hash值高16位不变,低16位与高16位异或作为key的最终hash值,由于n是正数,所以都是高位补0,最后返回一个比给定整数(cap)大且最接近的2的幂次方整数。

你可能和我一样又有疑问,这里为什么要”cap - 1“?

防止现在就是 2 的幂次,如果不减,经过下面的算法会把最高位后面的都置成 1,再加上一位 ”1” 则相当于将当前的数值乘 2

还是和2的幂次有关,在table下标的计算中也是同样,来看看这一段代码:

(在getNode方法中通过(n - 1) & hash来计算table的下标index的)
在这里插入图片描述
其中n是table的长度,table的长度都是2的幂,因此index仅与hash值的低n位有关(此n非table.leng,而是2的幂指数),hash值的高位都被与操作置为0了。也只有当 n为 2 的幂次方的时候,减一之后就会得到 “ 1111* ” 的数字,这个数字正好可以掩码,此时也正体现的掩码的作用,因为&操作,只有两边都是1才会得1,利用“1111*”掩码能够大大的提高散列的范围。

这个减一就是做为一个“低位掩码”的作用。

所以,我们在给HashMap设置容量初始值得时候可以利用tableSizeFor方法的思想来进行设置,这也正是阿里巴巴开发手册所要求的一点。

下篇文章将对HashMap的元素操作实现进行分析:)

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值