HashMap中是如何计算索引的呢?
其实大体上可以分类两步:
- 计算hash值
- 根据hash值计算元素的下标
对应的源码:
1、计算hash值:
// 最终返回的h就是hash值
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
2、根据hash值计算元素的下标
// n表示数组的长度,i就是最终计算出来的元素应该存放的下标
p = tab[i = (n - 1) & hash]
添加的元素应该存放的数组中的下标,是通过key的hash值和容器的大小减1,两者进行与运算,获取容器数组下标。
这里使用与运算,其实蕴含了一个隐藏条件,即数组的大小n,必须是2的n次方,否则,计算出来的下标值i是无法覆盖这个范围[0, n-1]的。
为什么必须是2的n次方呢?
首先我们需要先明确,我们计算下标需要尽量让元素更加分散,减少hash冲突。
那我们来看看如果数组的大小不是2的n次方会怎么样。
假设有一个长度为10的数组
,不是2的幂
那么此时它的n-1=9
,二进制表示9就是1001
,我们可以发现任何值与该值进行与运算,都无法改变中间的两个0,只能改变首尾的两个1,因此结果范围就缩小了一倍。
什么意思呢?
我们来模拟看看,随便一个hash,计算hash&(n - 1)的结果
假设的hash值 | hash&(n - 1)计算得到的下标 | 十进制下标 |
---|---|---|
0000、0110、0100、0010 | 0000 | 0 |
0001、0111、0101、0011 | 0001 | 1 |
1000、1110、1100、1010 | 1000 | 8 |
1001、1111、1101、1011 | 1001 | 9 |
这时候我们就可以发现问题了,原本我们数组的长度是10,可以使用的下标是[0,9]
,但是我们只用上了4个,很多hash值都发生了hash冲突
我们再来看看数组的大小为2的n次方的情况
假设我们的数组长度为16
,是2的4次方,那n-1=15
,对应的二进制就是1111
假设的hash值 | hash&(n - 1)计算得到的下标 | 十进制下标 |
---|---|---|
0000 | 0000 | 0 |
0001 | 0001 | 1 |
0010 | 0010 | 2 |
0011 | 0011 | 3 |
0100 | 0100 | 4 |
0101 | 0101 | 5 |
0110 | 0110 | 6 |
0111 | 0111 | 7 |
1000 | 1000 | 8 |
1001 | 1001 | 9 |
1010 | 1010 | 10 |
1011 | 1011 | 11 |
1100 | 1100 | 12 |
1101 | 1101 | 13 |
1110 | 1110 | 14 |
1111 | 1111 | 15 |
可以看到[0,15]
的下标都充分使用了,相比较数组长度不是2的n次方的利用就更加充分了
实际上,只要数组大小是2的幂,则 (n-1) & hash 的结果等效于: hash % (n-1);即与运算、求余运算通过这个前提,实现了等效。
而计算机中,与运算和求余运算,两个计算的效率肯定是前者更好。因为与运算,是直接对位进行逻辑与操作,属于cpu的底层支持的基础逻辑操作,但是求余运算可不是,应该是需要额外的算术运算单元支持的。可能这就是把数组大小规定为2的幂的原因之一,提高运算效率。