提示:本篇文章为上篇文章的延续,需要理解上篇文章内容才能对本篇内容进行理解。
前篇回顾: 上篇文章我们讲解了
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的元素操作实现进行分析:)