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中再次优化了这个哈希函数,把 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 侵删。
经过上边的分析咱们能够到以下结论: