HashMap是Java集合框架中最常用的Map实现,其设计精妙且性能优越。下面我将从数据结构、哈希算法、扩容机制、源码分析四个维度为你全面解析。
一、HashMap底层数据结构演进
📊 JDK 1.7 vs JDK 1.8 对比
| 特性 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 底层结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 链表插入方式 | 头插法 | 尾插法 |
| 扩容时链表处理 | 重新计算hash | 保持原有顺序(高低位分离) |
| 并发问题 | 头插法导致死循环 | 尾插法避免死循环 |
| 性能优化 | 无 | 链表转红黑树(阈值8) |
🏗️ JDK 1.8 底层数据结构详解
java
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 核心字段
transient Node<K,V>[] table; // 底层数组(哈希桶数组)
transient int size; // 实际元素个数
int threshold; // 扩容阈值(capacity * loadFactor)
final float loadFactor; // 负载因子(默认0.75)
transient int modCount; // 修改次数(fail-fast机制)
// 链表节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // key的hash值
final K key; // 键
V value; // 值
Node<K,V> next; // 指向下一个节点(链表)
}
// 红黑树节点(继承自LinkedHashMap.Entry)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left; // 左子节点
TreeNode<K,V> right; // 右子节点
TreeNode<K,V> prev; // 前驱节点
boolean red; // 颜色(红/黑)
}
}
📐 数据结构图示
java
+-----------------------------------+
| 哈希桶数组 (table) |
+-----------------------------------+
| index | 内容 |
+-------+----------------------------+
| 0 | null |
| 1 | Node(key1, value1) |
| 2 | TreeNode(root) | ← 红黑树(链表长度≥8)
| | / \ |
| | T1 T2 |
| 3 | Node → Node → Node | ← 链表(长度<8)
| 4 | null |
| ... | ... |
+-------+----------------------------+
核心设计:
- 数组:快速定位桶位置(时间复杂度O(1))
- 链表:处理哈希冲突(同一桶位置的多个元素)
- 红黑树:当链表过长时转换,提升查询性能(时间复杂度O(log n))
二、哈希算法与索引计算
🔢 哈希值计算流程
java
// 1. 计算key的hashCode
int h = key.hashCode();
// 2. 扰动函数:高位参与运算,减少哈希冲突
h = h ^ (h >>> 16);
// 3. 计算数组索引(与运算代替取模,性能更高)
int index = h & (table.length - 1);
📊 扰动函数详解
java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为什么需要扰动函数?
| 场景 | 无扰动函数 | 有扰动函数 |
|---|---|---|
| 哈希值 | 1111 1111 1111 1111 0000 0000 0000 0001 |
同左 |
| 数组长度-1 | 0000 0000 0000 0000 0000 1111 1111 1111 (1023) |
同左 |
| 与运算结果 | 0000 0000 0000 0000 0000 0000 0000 0001 |
0000 0000 0000 0000 0000 1111 1111 1110 |
| 问题 | 仅低位参与,冲突率高 | 高位也参与,分布更均匀 |
扰动函数的作用:将高位的特征"扰动"到低位,使哈希值分布更均匀,减少冲突。
🧪 哈希冲突示例
java
public class HashCollisionDemo {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
// 三个字符串的hashCode经过扰动后,可能落在同一桶
String key1 = "Aa";
String key2 = "BB";
String key3 = "C#";
System.out.println("key1 hashCode: " + key1.hashCode() + ", hash: " + hash(key1));
System.out.println("key2 hashCode: " + key2.hashCode() + ", hash: " + hash(key2));
System.out.println("key3 hashCode: " + key3.hashCode() + ", hash: " + hash(key3));
map.put(key1, "Value1");
map.put(key2, "Value2");
map.put(key3, "Value3");
// 这三个key可能落在同一个桶,形成链表
}
static int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
}
三、put()方法完整流程(含时序图)
📊 put()方法时序图

🔍 put()方法源码详解
java
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 如果table为空或长度为0,调用resize()初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算桶索引,并检查该位置是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 直接放入
else {
Node<K,V> e; K k;
// 3. 检查第一个节点是否是目标key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4. 如果是红黑树节点,调用红黑树的put方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5. 遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 尾插法添加新节点
p.next = newNode(hash, key, value, null);
// 检查是否需要转红黑树(阈值8)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到相同key,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 6. 如果找到相同key,替换value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // LinkedHashMap回调
return oldValue;
}
}
// 7. 修改次数+1(fail-fast)
++modCount;
// 8. 检查是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // LinkedHashMap回调
return null;
}
四、扩容机制(resize)详解
📊 扩容时序图
🔍 resize()源码详解
java
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// ========== 第一阶段:计算新容量和新阈值 ==========
if (oldCap > 0) {
// 超过最大容量,不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新容量 = 旧容量 * 2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 新阈值 = 旧阈值 * 2
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 首次扩容:容量16,阈值12(16 * 0.75)
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// ========== 第二阶段:创建新数组并迁移数据 ==========
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 遍历旧数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // 释放引用,便于GC
// 情况1:桶中只有一个节点
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 情况2:红黑树节点
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 情况3:链表节点(核心优化点)
else {
// 高低位分离
Node<K,V> loHead = null, loTail = null; // 低位链表(原索引)
Node<K,V> hiHead = null, hiTail = null; // 高位链表(原索引+oldCap)
Node<K,V> next;
do {
next = e.next;
// 关键:判断是否需要移动到新位置
if ((e.hash & oldCap) == 0) {
// 低位:保持原索引
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
// 高位:索引 + oldCap
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 放入新数组
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead; // 原索引位置
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead; // 原索引 + oldCap
}
}
}
}
}
return newTab;
}
📐 高低位分离原理图解
假设:
- 旧容量:16(二进制
0001 0000) - 新容量:32(二进制
0010 0000) - 哈希值:
0101 0101
java
旧索引计算:hash & (16-1) = 0101 0101 & 0000 1111 = 0101 (5)
扩容后索引计算有两种可能:
1. 保持原索引:0101 0101 & 0001 1111 = 0101 (5)
2. 原索引 + oldCap:0101 0101 & 0001 1111 = 10101 (21)
判断条件:hash & oldCap
- 如果 (hash & 16) == 0 → 保持原索引(低位)
- 如果 (hash & 16) != 0 → 原索引 + 16(高位)
示例:
hash1 = 0000 0101 → & 0001 0000 = 0 → 低位 → 索引5
hash2 = 0001 0101 → & 0001 0000 = 1 → 高位 → 索引21
优势:
- 无需重新计算hash
- 保持链表原有顺序(避免JDK 1.7头插法的死循环问题)
- 时间复杂度O(n),每个节点只需判断一次
五、链表转红黑树机制
📊 转换阈值
| 操作 | 阈值 | 说明 |
|---|---|---|
| 链表→红黑树 | 8 | 链表长度≥8且数组长度≥64 |
| 红黑树→链表 | 6 | 红黑树节点数≤6 |
java
// 阈值常量
static final int TREEIFY_THRESHOLD = 8; // 链表转树阈值
static final int UNTREEIFY_THRESHOLD = 6; // 树转链表阈值
static final int MIN_TREEIFY_CAPACITY = 64; // 转树最小数组容量
🔍 链表转红黑树流程
java
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 优化:如果数组长度小于64,优先扩容而不是转树
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
// 1. 将链表节点转换为TreeNode
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
// 2. 构建红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
📐 红黑树结构示例
java
桶索引:5
+-------------------+
| 红黑树根节点 |
| (key50) |
+---------+---------+
|
+---------+---------+
| |
(key30)红 (key70)黑
/ \ / \
(key20) (key40) (key60) (key80)
红黑树特性:
- 查询时间复杂度:O(log n)
- 插入/删除时间复杂度:O(log n)
- 相比链表(O(n)),在数据量大时性能显著提升
六、get()方法流程
java
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 1. 检查table是否为空,桶是否为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 2. 检查第一个节点
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 3. 遍历后续节点
if ((e = first.next) != null) {
// 3.1 红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 3.2 链表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
七、性能分析与最佳实践
负载因子影响
java
// 负载因子对比
HashMap<String, String> map1 = new HashMap<>(16, 0.5f); // 容量8时扩容
HashMap<String, String> map2 = new HashMap<>(16, 0.75f); // 容量12时扩容(默认)
HashMap<String, String> map3 = new HashMap<>(16, 1.0f); // 容量16时扩容
| 负载因子 | 扩容频率 | 空间利用率 | 哈希冲突概率 |
|---|---|---|---|
| 0.5 | 高 | 低 | 低 |
| 0.75 | 中 | 中 | 中(默认推荐) |
| 1.0 | 低 | 高 | 高 |
✅ 最佳实践
1. 预估容量,避免频繁扩容
java
// ❌ 不推荐:未知容量,频繁扩容
Map<String, User> userMap = new HashMap<>();
for (User user : userList) {
userMap.put(user.getId(), user);
}
// ✅ 推荐:预估容量
// 公式:expectedSize / loadFactor + 1
int initialCapacity = (int) (userList.size() / 0.75f) + 1;
Map<String, User> userMap = new HashMap<>(initialCapacity);
for (User user : userList) {
userMap.put(user.getId(), user);
}
2. 使用合适的负载因子
java
// 空间敏感场景:降低负载因子
Map<String, String> cache = new HashMap<>(1024, 0.5f);
// 查询频繁场景:保持默认0.75
Map<String, String> config = new HashMap<>();
3. 避免使用可变对象作为key
java
// ❌ 危险:修改key的hashCode会导致无法找到
class BadKey {
private String name;
// 有setter方法,hashCode可能改变
}
// ✅ 推荐:使用不可变对象
class GoodKey {
private final String id;
// 无setter,hashCode不变
}
4. 重写equals和hashCode
java
public class User {
private String id;
private String name;
@Override
public int hashCode() {
return Objects.hash(id); // 只用id计算
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof User)) return false;
User other = (User) obj;
return Objects.equals(id, other.id);
}
}
八、HashMap vs Hashtable vs ConcurrentHashMap
| 特性 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | ❌ 否 | ✅ 是(synchronized) | ✅ 是(CAS + synchronized) |
| null键/值 | ✅ 支持 | ❌ 不支持 | ✅ 支持(JDK 1.8) |
| 底层结构 | 数组+链表+红黑树 | 数组+链表 | 数组+链表+红黑树 |
| 扩容机制 | 2倍扩容 | 2倍扩容 | 2倍扩容 |
| 性能 | 高 | 低(全表锁) | 高(分段锁/CAS) |
| 推荐使用 | 单线程 | 不推荐 | 多线程 |
九、总结:核心要点
🔑 核心机制总结
| 机制 | 关键点 | 作用 |
|---|---|---|
| 哈希算法 | 扰动函数 h ^ (h >>> 16) |
减少哈希冲突 |
| 索引计算 | (n-1) & hash |
快速定位桶位置 |
| 扩容机制 | 2倍扩容 + 高低位分离 | 保持链表顺序,避免死循环 |
| 链表转树 | 长度≥8且容量≥64 | 提升查询性能 |
| 负载因子 | 默认0.75 | 平衡空间与时间 |