浅谈HashMap底层实现原理
Hashmap是java日常开发中最常用的集合类之一,它底层基于散列算法实现,注意:hashmap类是一个非线程安全类,在多线程环境使用时可能会出现线程安全问题。Hashmap源码细节很多,本文将记录作者学习部分常用方法时对底层实现原理的理解。
hashmap的数据结构图如图所示:

在jdk1.8之前Hashmap底层依赖数组+链表实现,在jdk1.8后引入红黑树解决链表过长降低效率的问题。
下面是方法实现部分
Hashmap类底层有四个构造方法,第一个构造方法很简单,仅将 loadFactor 变量设为默认值。构造方法2调用了构造方法3,而构造方法3仍然只是设置了一些变量。构造方法4则是将另一个 Map 中的映射拷贝一份到自己的存储结构中来,这个方法不是很常用。
我们在一般情况下,都会使用无参构造方法创建 HashMap。但当我们对时间和空间复杂度有要求的时候,使用默认值有时可能达不到我们的要求,这个时候我们就需要手动调参。在 HashMap 构造方法中,可供我们调整的参数有两个,一个是初始容量 initialCapacity,另一个负载因子 loadFactor。通过这两个设定这两个参数,可以进一步影响阈值大小。但初始阈值 threshold 仅由 initialCapacity 经过移位操作计算得出。他们的作用分别如下:
| 名称 | 用途 | |
|---|---|---|
| initialCapacity | HashMap 初始容量 | |
| loadFactor | 负载因子 | |
| threshold | 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容 |
HashMap 中没有定义 initialCapacity 这个变量,如果大家仔细看 HashMap 的构造方法,会发现存储键值对的数据结构并不是在构造方法里初始化的。
默认情况下,HashMap 初始容量是16,负载因子为 0.75。这里并没有默认阈值,原因是阈值可由容量乘上负载因子计算而来(注释中有说明),即threshold = capacity * loadFactor
初始化threshold的方法为tableSizeFor,该方法的作用为返回一个大于或等于传入值的最小2的幂。
再来说说负载因子(loadFactor)。对于 HashMap 来说,负载因子是一个很重要的参数,该参数反应了 HashMap 桶数组的使用情况(假设键值对节点均匀分布在桶数组中)。通过调节负载因子,可使 HashMap 时间和空间复杂度上有不同的表现。当我们调低负载因子时,HashMap 所能容纳的键值对数量变少。扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间。相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。至于负载因子怎么调节,这个看使用场景了。一般情况下,我们用默认值就可以了。
查找:
先定位键值对所在的桶的位置,然后再对链表或红黑树进行查找。
遍历:
遍历所有的键时,首先要获取键集合KeySet对象,然后再通过 KeySet 的迭代器KeyIterator进行遍历。KeyIterator 类继承自HashIterator类,核心逻辑也封装在 HashIterator 类中。HashIterator 的逻辑并不复杂,在初始化时,HashIterator 先从桶数组中找到包含链表节点引用的桶。然后对这个桶指向的链表进行遍历。遍历完成后,再继续寻找下一个包含链表节点引用的桶,找到继续遍历。找不到,则结束遍历。
插入:
插入操作的入口方法是 put(K,V),但核心逻辑在V putVal(int, K, V, boolean, boolean) 方法中。putVal 方法主要做了这么几件事情:
当桶数组 table 为空时,通过扩容的方式初始化 table(这里table为延迟初始化步骤,直到插入新数据才初始化table)
查找要插入的键值对是否已经存在,存在的话根据条件判断是否用新值替换旧值
如果不存在,则将键值对链入链表中,并根据链表长度决定是否将链表转为红黑树
判断键值对数量是否大于阈值,大于的话则进行扩容操作
扩容:
计算新桶数组的容量 newCap 和新阈值 newThr
根据计算出的 newCap 创建新的桶数组,桶数组 table 也是在这里进行初始化的
将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通节点,则节点按原顺序进行分组。
浙公网安备 33010602011771号