HashMap 是 Java 集合框架中最常用的键值对存储容器 ,基于「数组 + 链表 + 红黑树」实现,核心特点是无序、键唯一、值可重复、允许键 / 值为 null,底层通过哈希算法实现高效的增删改查(平均时间复杂度 O (1))。
一、核心特性
| 特性 | 说明 |
|---|---|
| 存储结构 | 数组(哈希桶)+ 链表(解决哈希冲突)+ 红黑树(优化极端冲突) |
| 无序性 | 无序(不保证插入 / 遍历顺序,底层按哈希值分布),如需有序用 LinkedHashMap |
| 键值规则 | 键唯一(重复键会覆盖值),值可重复;键 / 值均允许为 null |
| 线程安全 | 非线程安全(并发操作可能导致数据覆盖、死循环(Java 7)、遍历异常),并发场景用 ConcurrentHashMap |
| 初始容量 / 加载因子 | 默认初始容量 16,加载因子 0.75;扩容阈值 = 容量 × 加载因子 |
| 扩容规则 | 容量始终为 2 的幂,扩容时容量翻倍 |
二、底层结构解析
1. 核心组成
HashMap 的底层核心是「哈希桶数组 + 链表 / 红黑树」:
- 哈希桶数组(table) :数组类型为
Node[](Java 8),每个数组元素(哈希桶)对应一个链表 / 红黑树的头节点; - Node 节点 :链表节点,包含
hash(哈希值)、key、value、next(下一个节点引用); - TreeNode 节点:红黑树节点(继承 Node),用于链表转红黑树后存储,包含红黑树的左右子节点、父节点等属性。
2. Java 7 和 Java 8 区别
| 版本 | 存储结构 | 核心优化 |
|---|---|---|
| Java 7 | 数组 + 链表 | 头插法、多次哈希混合 |
| Java 8 | 数组 + 链表 + 红黑树 | 尾插法、红黑树优化、扩容下标优化 |
红黑树触发条件:
- 链表长度 ≥ 8 且 数组长度 ≥ 64 → 链表转红黑树;
- 红黑树节点数 ≤ 6 → 红黑树转回链表(避免红黑树维护成本)。
三、核心原理
1. 哈希值计算与下标映射
HashMap 为了让哈希值分布更均匀,分两步计算元素在数组中的下标:
步骤 1:计算混合哈希值(减少冲突)
java
static final int hash(Object key) {
int h;
// key为null时hash=0;否则混合hashCode的高低位
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 目的:将
hashCode()(32 位)的高 16 位与低 16 位异或,让高位特征渗透到低位,减少哈希冲突; - 原因:数组下标仅取哈希值的低 n 位(数组长度为 2ⁿ),混合高低位能提升下标分布均匀性。
步骤 2:计算数组下标(高效映射)
java
int index = hash & (table.length - 1);
- 原理:当数组长度为 2 的幂时,
hash & (length-1)等价于hash % length,但位运算效率更高; - 示例:长度 16(10000)→ length-1=15(01111),按位与后仅保留哈希值的最后 4 位,下标范围 0~15。
2. 核心操作流程
(1)put 操作(插入键值对)
put(K key, V value) 的核心是:计算哈希→定位桶→处理冲突→扩容检查,具体步骤如下:
put(key, value)
├─ 计算 hash = hash(key)
├─ 若 table 未初始化 → resize() 初始化
├─ 计算 index = (n-1) & hash
├─ 取 table[index] 为 p
│ ├─ p == null → 新建节点放入 table[index]
│ ├─ p 是目标节点 → 覆盖 value,返回旧值
│ └─ p 不是目标节点
│ ├─ 若 p 是红黑树 → 树插入/替换
│ └─ 若 p 是链表 → 遍历链表
│ ├─ 找到匹配 key → 覆盖 value
│ ├─ 未找到 → 新增节点到链表尾部
│ └─ 链表长度 ≥8 → 转红黑树(满足数组条件)
├─ size +1
└─ 若 size > threshold → resize() 扩容
(2)get 操作(查询值)
get(Object key) 的核心是:计算哈希→定位桶→遍历查找,步骤更简洁,无扩容 / 结构转换逻辑。
get(key)
├─ 若 key == null → 遍历桶 0 找 null key 的节点
├─ 计算 hash = hash(key)
├─ 计算 index = (n-1) & hash
├─ 取 table[index] 为 p
│ ├─ p == null → 返回 null
│ ├─ p 是目标节点 → 返回 p.value
│ └─ p 不是目标节点
│ ├─ 若 p 是红黑树 → 树查找,返回 value/null
│ └─ 若 p 是链表 → 遍历链表,返回 value/null
└─ 未找到 → 返回 null
(3)resize 操作(扩容)
扩容是 HashMap 性能瓶颈,触发条件:size(元素数)≥ 阈值(容量×加载因子)。
- Java 7:头插法迁移元素,并发下易导致链表成环;
- Java 8 优化:
- 尾插法迁移,避免链表成环;
- 下标重计算优化:新下标 = 原下标 或 原下标 + 原容量(仅判断哈希值的某一位,无需重新计算哈希);
- 懒加载:首次 put 时才初始化数组(Java 7 是 new HashMap 时初始化)。
3. 哈希冲突解决
HashMap 用「链地址法」解决冲突:
- 当多个 key 的哈希下标相同时,将这些节点组成链表;
- Java 8 中链表过长时转为红黑树,将查询时间复杂度从 O (n) 降为 O (logn)。
四、关键参数与优化
1. 核心参数
| 参数 | 默认值 | 说明 |
|---|---|---|
| DEFAULT_INITIAL_CAPACITY | 16 | 默认初始容量(必须是 2 的幂) |
| DEFAULT_LOAD_FACTOR | 0.75f | 默认加载因子(平衡空间与时间,0.75 是统计最优值) |
| TREEIFY_THRESHOLD | 8 | 链表转红黑树的阈值 |
| UNTREEIFY_THRESHOLD | 6 | 红黑树转回链表的阈值 |
| MIN_TREEIFY_CAPACITY | 64 | 链表转红黑树的最小数组容量(避免小数组频繁转树) |
2. 性能优化建议
(1)初始化指定容量
避免多次扩容:若已知存储 N 个元素,初始化容量 = N / 加载因子(向上取整为 2 的幂)。示例:存储 1000 个元素 → 1000 / 0.75 ≈ 1334 → 取最近的 2 的幂 2048(或直接传 1334,HashMap 会自动调整为 2048)。
java
// 推荐:指定初始容量,减少扩容
Map<String, Integer> map = new HashMap<>(2048);
(2)避免使用易冲突的 key
- 自定义对象作为 key 时,必须重写
hashCode()和equals():hashCode():保证相同对象返回相同哈希值,不同对象尽量返回不同值;equals():保证 key 的逻辑相等(比如基于属性判断)。
(3)并发场景替换为 ConcurrentHashMap
- HashMap 非线程安全:并发 put 可能导致数据覆盖,Java 7 并发扩容易死循环;
- ConcurrentHashMap:Java 8 用 CAS + synchronized 实现高效并发,替代 HashMap。
(4)避免频繁修改 key 的哈希相关属性
若 key 的属性(如自定义对象的 id)修改,会导致 hashCode() 变化,无法通过 get 查到原值。
五、常见面试考点
1. HashMap 为什么线程不安全?
- Java 7:并发扩容易导致链表成环,get 操作死循环;
- Java 8:修复了成环问题,但并发 put 仍会出现数据覆盖(两个线程同时插入,一个覆盖另一个的节点)。
2. 为什么容量是 2 的幂?
- 保证
hash & (length-1)等价于取模,提升下标计算效率; - 扩容时只需判断哈希值的某一位,即可确定新下标,减少重新哈希成本。
3. 加载因子为什么是 0.75?
- 加载因子过小(如 0.5):扩容频繁,空间利用率低;
- 加载因子过大(如 1.0):哈希冲突概率高,查询效率低;
- 0.75 是统计得出的最优值,平衡空间和时间效率。
4. Java 8 对 HashMap 的核心优化?
- 链表转红黑树,优化极端冲突下的查询性能;
- 尾插法替代头插法,解决并发成环问题;
- 扩容下标优化,减少重新哈希成本;
- 简化哈希计算逻辑(仅一次高低位异或);
- 数组懒加载,减少初始化内存占用。
六、总结
HashMap 是 Java 中最核心的集合类之一,其设计核心是「哈希算法 + 冲突优化」:
- 底层结构从 Java 7 的「数组 + 链表」升级为 Java 8 的「数组 + 链表 + 红黑树」,解决了极端冲突下的性能问题;
- 哈希计算和下标映射的设计,兼顾了效率和分布均匀性;
- 实际使用中需注意初始化容量、线程安全、key 的哈希稳定性等问题,才能充分发挥其性能优势。