0
点赞
收藏
分享

微信扫一扫

JDK8中HashMap索引计算的巧妙设计

玉字璧 2022-04-16 阅读 52
java

HashMap中是如何计算索引的呢?
其实大体上可以分类两步:

  1. 计算hash值
  2. 根据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、001000000
0001、0111、0101、001100011
1000、1110、1100、101010008
1001、1111、1101、101110019

这时候我们就可以发现问题了,原本我们数组的长度是10,可以使用的下标是[0,9],但是我们只用上了4个,很多hash值都发生了hash冲突

我们再来看看数组的大小为2的n次方的情况
假设我们的数组长度为16,是2的4次方,那n-1=15,对应的二进制就是1111

假设的hash值hash&(n - 1)计算得到的下标十进制下标
000000000
000100011
001000102
001100113
010001004
010101015
011001106
011101117
100010008
100110019
1010101010
1011101111
1100110012
1101110113
1110111014
1111111115

可以看到[0,15]的下标都充分使用了,相比较数组长度不是2的n次方的利用就更加充分了
实际上,只要数组大小是2的幂,则 (n-1) & hash 的结果等效于: hash % (n-1);即与运算、求余运算通过这个前提,实现了等效。
而计算机中,与运算和求余运算,两个计算的效率肯定是前者更好。因为与运算,是直接对位进行逻辑与操作,属于cpu的底层支持的基础逻辑操作,但是求余运算可不是,应该是需要额外的算术运算单元支持的。可能这就是把数组大小规定为2的幂的原因之一,提高运算效率。

举报

相关推荐

0 条评论