Java-HashMap中的扰动函数、初始化容量、负载因子以及扩容链表拆分
1.扰动函数
在hashmap中,put操作是这样进行的:
其中会涉及到hash(key)
的运算,hash并不是直接使用hashCode(),而是这样:
这里的操作就称之为扰动函数
根据取模操作中的&和(length-1)这篇文章中提到的,计算出hash后我们可以使用&进行取模操作来确定放置在哪里。
现在问题是:计算hash时,为什么不直接用hashCode(),而是
先说结论:
增大随机性,优化散列效果,让数据元素更加均匀的散列(减少碰撞)。
1.1 实例
1.1.1 Hash的取值范围
在Java中,hashCode()
方法返回的哈希值是一个32位的有符号整数(int
类型)。
因此,哈希值的取值范围是从 [-2147483648, 2147483647]
(即 -2^31
到 2^31-1
)。
1.1.2 扰动函数的计算过程
对于扰动hash的函数的计算,做一个拆分。
测试用例
输出结果
上面的过程可以用下面的图来表示
1.2 总结
1.异或的特性是什么?
当使用异或运算符(^)对两个二进制数进行操作时,有以下基本规则:
-
相同为0:如果两个操作数的对应位相同,则结果为0。 例如:0 ^ 0 = 0,1 ^ 1 = 0
-
不同为1:如果两个操作数的对应位不同,则结果为1。 例如:0 ^ 1 = 1,1 ^ 0 = 1
2.通过将哈希值向右移动16位(h >>> 16),将原哈希值的高位移动到了低位。
将原来的高16位移到低16位后,高16位被0补位,全是0。这个时候,原来的高16位:
- 如果是0:0 ^ 0 = 0,则还是0
- 如果是1:1 ^ 0 = 1,则还是1
可以发现,高16位的结果并不会受影响。
3.高16位变成低16位后,与原来的低16位做异或运算。
md这里没想清楚,等我再悟一悟。
下面是个实际的例子。
- 10万个不重复的单词
- 128个格子,相当于128长度的数组。
现在就是说这10万个数据怎么放进去这128的数组里。
y值就是每个位置的数据量。
-
没有使用前,有的位置数据多,有的位置数据少,分布的不够均匀。
-
使用了扰动函数后,每个位置的数据量均衡许多。
2.初始化容量和负载因子
2.1 初始化容量
在Java-取模操作中的&和(length-1)这篇文章中提到过,为了方便快速取模,我们通常将数组大小设置为2n
现在有一个问题,如果在初始化HashMap时,我们将初始化容量设置为17会怎么样?
点进去这个构造方法。
可以看到调用 this(initialCapacity, DEFAULT_LOAD_FACTOR)
传递到另一个构造方法。
- loadFactor:负载因子
- tableSizeFor:阈值
tableSizeFor(initialCapacity)
用来计算阈值。
上面的注释里也能看到,最大容量是这个。
1 左移 30 位的结果是 1073741824。换句话说,
MAXIMUM_CAPACITY
的值被设置为 1073741824。
容量为啥不是2^32次方?
HashMap为了保持高效性和性能,限制了容量必须是2的幂次。
而2^32是一个超过Java中int类型表示范围的数值(231-1是int类型的最大值),所以HashMap的最大容量被限制为1 << 30,即2的30次方。
这里再补充一句,还记不记得Java中hashCode的取值范围?
在Java中,hashCode() 方法返回的哈希码(哈希值)是一个32位的整数,其取值范围是从[ -231,231-1],即[-2147483648,2147483647]。
废话完了,现在来解释下tableSizeFor这个函数,这个函数是为了找到最邻近输入值的那个最小2n。
看懂这个函数,最主要就是搞清楚|=
是个啥玩意?
|=
是位运算中的按位或赋值
操作符。它将右操作数的位值与左操作数的位值进行按位或运算,并将结果赋值给左操作数。
具体来说,n |= n >>> x
的含义是将 n
的高 x
位的值复制到低 x
位上,实现了一种类似于向上取整
的操作。
n |= n >>> 1
表示将 n
的高一半位的值复制到低一半位上,相当于将 n
的最高位复制到次高位上。
n |= n >>> 2
表示将 n
的高两半位的值复制到低两半位上,相当于将 n
的最高两位复制到次高两位上。
以此类推,n |= n >>> 4
、n |= n >>> 8
和 n |= n >>> 16
会将 n
的高半部分的值复制到低半部分的对应位置上。
以上面的17为例。
第一次就是把头一个1复制到第二个1,第二次就是把头两个1复制到次两个1...
有点感觉了对不,那为啥开始计算前,也就是最开始要减去1呢?
考虑到HashMap的容量必须是2的幂次,减去1可以确保在计算过程中不会出现直接得到小于
cap
的2的幂次值的情况。
假设进来直接是2n,没有做减法,我们写个函数验证下。
输出结果
哦豁,变32了,浪费了嘛,所以,要排除掉本身就是2n的情况。
2.2 负载因子
要选择一个合理的大小下进行扩容,默认值0.75,就是说。
当阈值容量占了3/4时赶紧扩容,减少Hash碰撞。
同时0.75是一个默认构造值,在创建HashMap也可以调整,如果你希望用更多的空间换取时间,可以把负载因子调的更小一些,减少碰撞。
是说数组(绿色)位置的元素数量超过loadFactor,还是所有的(绿色+紫色)元素数量超过loadFactor?
先说结论,是所有的元素,后文分析。
3.扩容元素拆分
__EOF__

本文链接:https://www.cnblogs.com/yang37/p/17507527.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具