一、核心特性
| 特性 | 说明 |
|---|---|
| 存储结构 | JDK7:数组 + 链表;JDK8:数组 + 链表/红黑树(链表过长时优化) |
| 键值规则 | 允许 null 键(仅1个,因为键唯一)和 null 值(多个) |
| 有序性 | 无序(插入顺序 ≠ 遍历顺序,底层哈希分布决定) |
| 线程安全 | 非线程安全(多线程操作易出现数据丢失、死循环、快速失败等问题) |
| 哈希冲突解决 | 链地址法(拉链法),JDK8 引入红黑树优化查询效率 |
| 快速失败(fail-fast) | 遍历过程中修改集合会抛出 ConcurrentModificationException |
| 初始容量/负载因子 | 默认初始容量16(2的幂),默认负载因子0.75,扩容阈值=容量×负载因子 |
二、底层实现(JDK7 vs JDK8 核心区别)
1. 存储结构差异
| 版本 | 核心结构 | 节点类型 | 插入方式 | 核心优化点 |
|---|---|---|---|---|
| JDK7 | 数组(Entry[] table)+ 链表 | Entry(key/value/hash/next) | 头插法 | 多次哈希扰动保证分布均匀 |
| JDK8 | 数组(Node[] table)+ 链表/红黑树 | Node(基础节点)/TreeNode(红黑树节点) | 尾插法 | 红黑树优化链表查询、简化哈希算法 |
2. 关键概念解释
- 数组(哈希桶):HashMap 的底层数组称为「哈希桶」,每个桶对应一个索引,存储链表/红黑树的头节点。
- 链表:解决哈希冲突(不同 key 计算出相同索引),冲突的节点链接成链表。
- 红黑树 :JDK8 新增,当链表长度 ≥ 8 且 数组长度 ≥ 64 时,链表转为红黑树(查询效率从 O(n) 优化为 O(logn));当红黑树节点数 ≤ 6 时,转回链表(避免红黑树维护成本)。
三、核心参数与哈希算法
1. 核心参数
| 参数名 | 默认值 | 作用说明 |
|---|---|---|
| initialCapacity | 16 | 初始容量(必须是2的幂,默认16),哈希桶的初始长度 |
| loadFactor | 0.75 | 负载因子:衡量数组填充程度的阈值,平衡「时间/空间」(0.75是统计最优值) |
| threshold | 12(16×0.75) | 扩容阈值:当元素数量 ≥ threshold 时,触发数组扩容 |
| MAXIMUM_CAPACITY | 2^30 | 最大容量(避免数组过大导致内存溢出) |
| modCount | - | 修改次数:用于快速失败机制,记录集合的增/删操作次数 |
2. 为什么初始容量是 2 的幂?
HashMap 中,key 的索引计算公式为:索引 = hash(key) & (容量 - 1)。
- 若容量是 2 的幂,
容量 - 1的二进制是全 1(如 16-1=15 → 1111),此时&运算等价于「取模运算」,但位运算效率远高于取模; - 全 1 的二进制能让 hash 值的每一位都参与运算,减少哈希冲突,让节点分布更均匀。
3. 哈希算法(JDK7 vs JDK8)
JDK7:多次扰动(4次位运算 + 5次异或)
java
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
- 目的:通过多次位运算打乱 hash 值的二进制,减少冲突。
JDK8:简化扰动(高16位 ^ 低16位)
java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 优化:保留高16位参与运算(低16位和高16位异或),既保证随机性,又降低计算成本;
- 最终索引:
hash & (capacity - 1)。
四、核心方法流程
1. put 方法(JDK8 核心流程)
HashMap 存储键值对的核心逻辑,步骤如下:
空
非空
空
非空
匹配
不匹配
是
否
找到
未找到
是且数组长度≥64
否
是
否
检查数组是否为空
调用resize()初始化/扩容
计算key的hash值,得到索引i
检查table[i]是否为空
创建Node节点放入table[i]
判断首节点是否匹配key(hash相等+equals相等)
替换value并返回旧值
判断是否是红黑树节点
调用putTreeVal()插入红黑树
遍历链表,查找匹配key
尾插法插入链表尾部
检查链表长度≥8?
链表转红黑树
不转换
size++,检查是否超过threshold
调用resize()扩容
结束
关键细节:
- JDK8 改用「尾插法」:解决 JDK7 头插法在并发扩容时导致的「循环链表」问题;
- 红黑树转换条件:链表长度 ≥8 且 数组长度 ≥64(若数组长度<64,先扩容而非转红黑树)。
2. get 方法(JDK8 核心流程)
根据 key 获取 value 的核心逻辑:
- 检查数组非空且目标索引有节点;
- 检查首节点是否匹配 key(hash+equals),匹配则返回 value;
- 不匹配则判断节点类型:
- 红黑树:调用
getTreeNode()按红黑树规则查找; - 链表:遍历链表查找,匹配则返回 value;
- 红黑树:调用
- 未找到则返回
null。
3. resize 方法(扩容流程)
当元素数量 ≥ threshold 时触发扩容,核心步骤:
- 计算新容量 :旧容量翻倍(
oldCap << 1),最大不超过 2^30; - 计算新阈值 :旧阈值翻倍(
oldThr << 1); - 转移节点 :创建新数组,将旧数组节点迁移到新数组:
- 链表节点:新索引 =
hash & (newCap - 1),结果只有两种可能:原索引 或 原索引 + 旧容量; - 红黑树节点:拆分红黑树,若拆分后节点数 ≤6,转回链表;
- 链表节点:新索引 =
- 替换旧数组为新数组,完成扩容。
五、哈希冲突解决
HashMap 采用「链地址法(拉链法)」解决哈希冲突:
- 原理:数组的每个索引对应一个链表/红黑树,哈希值相同的 key 被放入同一个链表/红黑树中;
- 对比其他冲突解决方式:
- 开放定址法:容易产生堆积,查询效率低;
- 再哈希法:计算成本高;
- 链地址法:无堆积、容错性高,是 HashMap 的最优选择。
六、线程安全问题
1. 非线程安全的表现
- JDK7 并发扩容:头插法导致链表循环,get 操作时死循环;
- 多线程 put:可能覆盖彼此的操作,导致数据丢失;
- 快速失败:遍历过程中修改集合,抛出
ConcurrentModificationException。
2. 线程安全替代方案
| 方案 | 实现原理 | 效率 | 推荐度 |
|---|---|---|---|
| Hashtable | 方法加 synchronized |
低(全局锁) | ⭐⭐ |
| Collections.synchronizedMap | 包装类,底层 synchronized |
中(全局锁) | ⭐⭐⭐ |
| ConcurrentHashMap | JDK7:分段锁;JDK8:CAS + synchronized | 高(局部锁) | ⭐⭐⭐⭐⭐ |
七、快速失败(fail-fast)机制
1. 原理
HashMap 内部维护 modCount(修改次数),迭代器遍历前会记录 expectedModCount = modCount,遍历过程中每次操作都会检查 modCount == expectedModCount:
- 相等:正常遍历;
- 不相等:抛出
ConcurrentModificationException。
2. 触发场景
- 遍历(迭代器/增强 for)时调用
put()/remove()(迭代器自身的remove()除外); - 多线程同时修改集合。
3. 注意
fail-fast 是「检测机制」,不保证 100% 触发(如修改操作未及时更新 modCount),仅用于暴露非线程安全问题。
八、关键注意点
1. 自定义类作为 Key 的要求
若自定义类作为 HashMap 的 Key,必须重写 hashCode() 和 equals():
- 重写
equals():保证逻辑上相等的对象被判定为相等; - 重写
hashCode():保证equals()相等的对象,hashCode()一定相等(否则会被当成不同 Key 存储)。
2. 不可变类适合作为 Key
推荐使用 String、Integer 等不可变类作为 Key:
- 不可变类的
hashCode()计算后不会变化,避免 Key 的 hash 值改变后无法查找; - 若 Key 是可变类(如自定义对象),修改其属性会导致
hashCode()变化,进而无法通过原 Key 获取 Value。
3. 负载因子的调整
- 负载因子调大:数组填充更满,空间利用率高,但哈希冲突概率增加,查询变慢;
- 负载因子调小:数组填充更空,查询更快,但空间浪费多;
- 场景适配:若内存充足、追求查询效率,可设为 0.5;若内存紧张、追求空间利用率,可设为 0.8(不建议超过0.85)。
九、常见面试高频问题
- HashMap 底层实现?(分 JDK7/8 讲数组+链表/红黑树,重点讲 JDK8 优化);
- 为什么初始容量是 2 的幂?(位运算效率高、哈希分布均匀);
- 负载因子为什么是 0.75?(时间/空间平衡,泊松分布统计得出冲突概率最低);
- JDK8 为什么引入红黑树?(链表查询 O(n),红黑树 O(logn),优化长链表查询);
- HashMap 和 ConcurrentHashMap 的区别?(线程安全、实现原理、性能);
- 为什么重写 equals 必须重写 hashCode?(保证相等对象的 hash 值一致,避免哈希冲突);
- JDK7 和 JDK8 HashMap 的核心区别?(结构、插入方式、哈希算法、扩容逻辑)。
十、核心区别总结(JDK7 vs JDK8)
| 维度 | JDK7 | JDK8 |
|---|---|---|
| 存储结构 | 数组 + 链表 | 数组 + 链表/红黑树 |
| 插入方式 | 头插法 | 尾插法 |
| 哈希算法 | 多次扰动(复杂) | 高16位^低16位(简化) |
| 扩容节点转移 | 重新计算 hash,头插 | 无需重算 hash,按索引迁移 |
| 并发问题 | 循环链表(死循环) | 无循环链表,但仍非线程安全 |
| 查询效率 | O(n)(链表) | O(logn)(红黑树) |