HashMap的数据结构是什么?
hashmap在Java8以后使用的是数组+链表+红黑树实现的。当插入一个数据时,会使用键的hash值经过扰动函数计算后与数组长度-1进行&运算,得到数据在数组的中的位置。如果该位置上已经有数据,则在下面形成链表,当链表长度超过8且数组长度超过64则链表变为红黑树。
HashMap是怎么扩容的?
容量变化:当存储内容超过数组长度*负载因子时发生扩容,默认长度为16,负载因子为0.75,即超过12个进行扩容,并且扩容的后的长度为2的次幂,如16扩容后为32。
变化过程:
- 创建新的数组:容量扩大为原容量的2倍(如16→32),保持2的幂次特性
- 重新计算位置
- 元素迁移:
java7使用头插法,可能导致链表倒置和并发环形链表问题。
java8使用尾插法,并根据哈希值高位将链表拆分为“低位链表”和“高位链表”,分别迁移到新数组的原位置和“原位置+旧容量”位置。若链表长度超过8且数组长度≥64,链表转为红黑树以提升查询效率
为什么扩容为2的次幂?
扩容为2的次幂的主要为了方便计算位置。正常计算应当是使用hash值与长度n进行取模运算,但是当长度n为2的次幂时,使用hash值与(n-1)进行&运算结果一样且效率更高。另外还可以避免n-1的二进制表示中间出现0,如9=0b1001,导致部分位置不参与计算,分布不均匀。
- 高效位运算代替取模
计算索引的公式为 index = hash & (n-1),当n为2次幂时,n-1的二进制全为1(如n=16时,n-1=15=0b1111)。此时,&运算等价于hash % n,但位运算效率比取模高10倍以上。
示例:当哈希值为0x3E6C8130,n=16时,0x3E6C8130 & 15 = 0,直接定位到数组第0个位置。 - 均匀分布哈希值
若n非2次幂,n-1的二进制可能包含0(如n=10时,n-1=9=0b1001),导致哈希值的某些位无法参与索引计算,分布不均匀。而2次幂保证所有低位均参与运算,减少哈希冲突。 - 简化扩容迁移逻辑
扩容后新索引仅需判断哈希值的某一位(如旧容量16对应的二进制高位第5位):
高位为0:索引不变(如旧索引5→新索引5)。
高位为1:索引变为“原索引+旧容量”(如旧索引5→新索引5+16=21)。
示例:哈希值10101100在旧数组(n=8)的索引为4(10101100 & 7=4),扩容后n=16,索引为12(10101100 & 15=12)。 - 兼容哈希扰动函数
HashMap通过扰动函数hash = key.hashCode() ^ (key.hashCode() >>> 16)将高位特征混合到低位,而2次幂的容量设计能充分利用扰动后的所有二进制位,避免仅依赖低位导致冲
HashMap是怎么解决hash冲突的?
- 扰动函数hash=key.hashcode()^(key.hashcode()>>>16)
分析: 扰动函数充分结合了高位与低位的特性,减少了因为hash值低位相同发生冲突。hash值为int类型,二进制表示为32位,直接使用hash&(n-1)进行计算时,n-1 高位一般为全为0,这样进行&运算,无论hash值高位是什么都会得到0,无法影响取值。反之将hash右移16位,这样高位就会变到低位,在与hash值进行异或运算(数据相同为0否则为1,如0000 0000 0000 0010 ^ 0000 0000 0000 0000 = 0000 0000 0000 0010)后得到一个结合高位与低位的hash值,这样再与n-1进行&运算,就避免了因为低位一致导致的hash冲突,从而减少了hash冲突。