Java HashMap采用数组+链表/红黑树结构,通过哈希计算和动态扩容实现高效键值存储,非线程安全,建议高并发场景使用ConcurrentHashMap。
一、核心特性
- 键值对存储:基于
key-value结构,允许null键和null值。 - 无序性:不保证元素顺序(
LinkedHashMap维护插入顺序)。 - 哈希冲突解决:链表 + 红黑树(JDK8+)。
- 非线程安全:多线程操作需同步或使用
ConcurrentHashMap。
二、底层数据结构(JDK8+)
1. 数组 + 链表 + 红黑树
-
数组(哈希桶) :
Node<K,V>[] table,每个桶存放链表或红黑树。 -
链表节点:
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // 链表指针 } -
红黑树节点(链表长度 ≥8 时树化):
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // 父节点 TreeNode<K,V> left; // 左子树 TreeNode<K,V> right; // 右子树 boolean red; // 颜色标记 }
2. 数据结构图示(Mermaid)
HashMap
桶1: 链表
数组
桶2: 红黑树
桶N: ...
三、关键实现原理
1. 哈希计算
-
哈希扰动函数(减少碰撞):
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } -
索引计算:
// 桶下标 = (数组长度 - 1) & hash int index = (n - 1) & hash;
2. put() 流程
UserHashMapput(key, value)计算 key 的 hash 值根据 hash 定位桶下标直接插入新 Node遍历链表/红黑树替换旧 value插入新 Node树化链表alt[链表长度≥8且数组长度≥64]alt[key已存在][key不存在]alt[桶为空][桶非空]检查是否需要扩容返回旧 value(或 null)UserHashMap
3. 扩容机制
-
触发条件:元素数量 > 容量 × 负载因子(默认
0.75)。 -
扩容规则:
-
新容量 = 旧容量 × 2(保证始终为 2 的幂)。
-
重新计算所有元素的桶下标(利用高位掩码优化):
// 例如旧容量为 16(二进制 10000),扩容后为 32(二进制 100000) if ((e.hash & oldCap) == 0) { // 新索引 = 原位置 } else { // 新索引 = 原位置 + oldCap }
-
四、线程安全问题
-
问题场景:
- 死循环(JDK7):多线程扩容时链表成环。
- 数据丢失:并发插入导致覆盖。
-
解决方案:
- 使用
ConcurrentHashMap。 - 通过
Collections.synchronizedMap()包装(性能较低)。
- 使用
五、性能优化技巧
-
初始化容量:
// 预估元素数量,避免频繁扩容 new HashMap<>(initialCapacity); -
哈希函数设计:
- 键对象必须正确重写
hashCode()和equals()。 - 避免哈希冲突(如
String、Integer等不可变对象作为键)。
- 键对象必须正确重写
-
负载因子调整:
// 降低负载因子(如 0.5)以减少冲突,但增加内存占用 new HashMap<>(16, 0.5f);
六、与其他 Map 实现对比
| 特性 | HashMap | LinkedHashMap | TreeMap | ConcurrentHashMap |
|---|---|---|---|---|
| 顺序 | 无序 | 插入顺序/访问顺序 | 排序 | 无序 |
| 底层结构 | 数组+链表/红黑树 | 哈希表 + 双向链表 | 红黑树 | 分段数组 + CAS |
| 线程安全 | 否 | 否 | 否 | 是 |
| 时间复杂度(平均) | O(1) | O(1) | O(log n) | O(1) |
| 适用场景 | 通用键值存储 | 需要顺序或 LRU 缓存 | 需要排序或范围查询 | 高并发场景 |
七、源码分析(关键方法)
1. putVal() 核心逻辑(简化版)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 初始化或扩容
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. 检查头节点是否匹配
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 红黑树插入
else {
// 4. 遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash); // 树化
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 5. 替换旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
}
// 6. 检查扩容
if (++size > threshold)
resize();
return null;
}
2. resize() 扩容逻辑
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 计算新容量和阈值...
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 迁移数据
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 树节点拆分
else {
// 链表拆分(lo/hi 链)
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
do {
if ((e.hash & oldCap) == 0) {
if (loTail == null) loHead = e;
else loTail.next = e;
loTail = e;
} else {
if (hiTail == null) hiHead = e;
else hiTail.next = e;
hiTail = e;
}
} while ((e = e.next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead; // 原索引位置
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead; // 新索引位置
}
}
}
}
return newTab;
}
八、总结
-
核心优势:高效的随机访问(平均 O(1) 时间复杂度)。
-
使用注意:
- 键对象必须正确实现
hashCode()和equals()。 - 避免在多线程环境中直接使用。
- 键对象必须正确实现
-
最佳实践:
- 预估初始容量,减少扩容开销。
- 高并发场景选择
ConcurrentHashMap。 - 需要顺序访问时使用
LinkedHashMap。