HashMap的长度为何要是2的n次方

HashMap为了存取高效,要尽可能较少碰撞,就是要尽可能把数据分配均匀,每一个链表长度大体相同,这个实现就在把数据存到哪一个链表中的算法;
这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中作了优化hash&(length-1),
hash%length==hash&(length-1)的前提是length是2的n次方;
为何这样能均匀分布减小碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1  实际就是n个1;
例如长度为9时候,3&(9-1)=0  2&(9-1)=0 ,都在0上,碰撞了;
例如长度为8时候,3&(8-1)=3  2&(8-1)=2 ,不一样位置上,不碰撞;html

 

其实就是按位“与”的时候,每一位都能  &1  ,也就是和1111……1111111进行与运算java

0000 0011     3算法

& 0000 1000      8数组

= 0000 0000      0函数

0000 0010     2优化

& 0000 1000    8spa

= 0000 0000      0code

 -------------------------------------------------------------htm

0000 0011     3图片

& 0000 0111     7

= 0000 0011       3

0000 0010      2

& 0000 0111      7

= 0000 0010      2

固然若是不考虑效率直接求余便可(就不须要要求长度必须是2的n次方了)

 

JDK1.8 中 hash 函数的实现

JDK1.8中再次优化了这个哈希函数,把 key 的 hashCode 方法返回值右移16位,即丢弃低16位,高16位全为0 ,而后在于 hashCode 返回值作异或运算,即高 16 位与低 16 位进行异或运算,这么作能够在数组 table 的 length 比较小的时候,也能保证考虑到高低Bit都参与到 hash 的计算中,同时不会有太大的开销,扰动处理次数也从 4次位运算 + 5次异或运算 下降到 1次位运算 + 1次异或运算

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

进过上述的扰动函数只是获得了合适的 hash 值,可是尚未肯定在 Node[] 数组中的角标,在 JDK1.7中存在一个函数,JDK1.8中虽然没有可是只是把这步运算放到了 put 函数中。咱们就看下这个函数实现:

static int indexFor(int h, int length) {
     return h & (length-1);  // 取模运算
}
复制代码

为了让 hash 值可以对应到现有数组中的位置,咱们上篇文章讲到一个方法为 取模运算,即 hash % length,获得结果做为角标位置。可是 HashMap 就厉害了,连这一步取模运算的都优化了。咱们须要知道一个计算机对于2进制的运算是要快于10进制的,取模算是10进制的运算了,而位与运算就要更高效一些了。

咱们知道 HashMap 底层数组的长度老是 2^n ,转为二进制老是 1000 即1后边多个0的状况。此时一个数与 2^n 取模,等价于 一个数与 2^n - 1作位与运算。而 JDK 中就使用h & (length-1) 运算替代了对 length取模。咱们根据图片来看一个具体的例子:

 

 

图片来自:https://tech.meituan.com/java-hashmap.html 侵删。

小结

经过上边的分析咱们能够到以下结论:

  • 在存储元素以前,HashMap 会对 key 的 hashCode 返回值作进一步扰动函数处理,1.7 中扰动函数使用了 4次位运算 + 5次异或运算,1.8 中下降到 1次位运算 + 1次异或运算
  • 扰动处理后的 hash 与 哈希表数组length -1 作位与运算获得最终元素储存的哈希桶角标位置。
相关文章
相关标签/搜索