在 Java 开发中,HashMap 是使用频率极高的集合类,理解其底层原理对性能优化和问题排查至关重要。接下来从数据结构、容量机制、操作流程等多个维度,深入剖析 HashMap 的运行机制。
博主总结
一、数据结构:数组 + 链表 + 红黑树
HashMap 采用数组 + 链表 + 红黑树的复合结构。数组作为主体存储数据,每个数组元素称为一个 “桶”;当发生哈希冲突时,冲突元素通过链表形式存储在对应桶中。当链表长度超过 8 且数组长度大于 64 时,链表会转换为红黑树,将查询时间复杂度从链表的 O (n) 优化至红黑树的 O (logn),大幅提升查询效率。
二、容量机制:初始容量与扩容
- 初始容量:HashMap 默认初始容量为 16,若初始化时传入非 2 的幂次方值(如 17),会自动调整为大于等于该值的最小 2 的幂次方(即 32)。
- 扩容机制:当键值对数量超过 ** 容量 × 负载因子(默认 0.75)** 时触发扩容。扩容时,创建容量为原来 2 倍的新数组,并将旧数组元素重新分配到新数组。JDK 8 采用尾插法,通过(e.hash & oldCap) == 0判断元素位置,避免重新计算哈希值,提高扩容性能。
三、核心操作流程
put 流程
- 哈希寻址:通过哈希函数计算键的哈希值,并与数组长度 - 1 进行按位与运算,确定元素所在桶的索引。
- 判断桶状态:若桶为空,直接插入;若不为空,判断是链表还是红黑树。
- 处理冲突:链表结构按顺序遍历插入,红黑树按树结构插入;若键已存在,则覆盖旧值。
- 扩容判断:插入后检查键值对数量是否超过阈值,超过则触发扩容。
get 流程
- 通过哈希值定位数组索引,找到对应桶。
- 检查桶中第一个节点,若匹配则直接返回;否则根据节点类型遍历链表或红黑树查找,找到则返回结果,未找到返回 null。
四、哈希冲突处理与优化
减少哈希冲突
- 哈希值计算:通过高位与低位异或运算,让哈希值更均匀分布。
- 索引计算:利用h & (n - 1)(n 为数组长度且是 2 的幂次方)按位与运算替代取模运算,提高计算效率。
解决哈希冲突方法
- 拉链法:HashMap 采用的方法,用链表处理冲突,链表过长时转换为红黑树,扩容时树元素个数小于 6 则退化为链表。
- 再哈希法:准备多套哈希算法,冲突时切换算法直至找到空槽,对算法设计要求高。
- 开放地址法:包括线性探测(顺序查找空槽)、二次探测(按平方步长查找)、双重哈希(使用备用哈希函数)。
五、线程安全性对比
- HashMap:线程不安全,多线程环境下可能出现数据覆盖、死循环等问题。
- Hashtable:通过synchronized修饰方法实现线程安全,但锁粒度大,性能差。
- ConcurrentHashMap:Java 8 后采用CAS 操作 + synchronized 锁,写操作通过 CAS 插入节点,冲突时对链表 / 红黑头结点加锁;读操作大多无锁,通过volatile保证可见性,性能更优,推荐使用。
完结。