源码及原理解析
1. HashMap 概述
HashMap 是 Java 集合框架中最重要的数据结构之一,基于哈希表实现,提供了键值对的存储和检索功能。
1.1 核心特性
- 键值对存储:存储 Key-Value 对
- 允许 null:key 和 value 都允许为 null
- 非线程安全:多线程环境下需要外部同步
- 无序性:不保证元素的顺序
- 时间复杂度:理想情况下 O(1) 的查询效率
2. 数据结构演变
2.1 数组 + 链表
JDK 1.7 及之前:
// JDK 1.7 的节点结构
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
2.2 数组 + 链表/红黑树
JDK 1.8 及之后:
// JDK 1.8 的节点结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
// 红黑树节点
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;
}
3. 核心源码解析
3.1 关键常量
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 默认初始容量 - 必须是 2 的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组
transient Node<K,V>[] table;
// 元素个数
transient int size;
// 修改次数
transient int modCount;
// 扩容阈值 (capacity * loadFactor)
int threshold;
// 负载因子
final float loadFactor;
}
3.2 哈希计算
static final int hash(Object key) {
int h;
// key 为 null 时哈希值为 0,否则计算哈希值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 计算数组下标
final int indexFor(int hash, int length) {
// 等价于 hash % length,但效率更高
return hash & (length - 1);
}
3.3 put 方法实现
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 为空,进行初始化
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;
// 4. 如果是树节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 5. 链表遍历
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表长度达到阈值,转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
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);
return oldValue;
}
}
++modCount;
// 7. 检查是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3.4 扩容机制 (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;
// 计算新容量和新阈值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 双倍阈值
}
else if (oldThr > 0) // 初始容量设置为阈值
newCap = oldThr;
else { // 零初始阈值表示使用默认值
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;
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 { // 链表
// 低位链表和高位链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
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 {
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;
}
}
}
}
}
return newTab;
}
3.5 get 方法实现
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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 检查第一个节点
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 如果是树节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 遍历链表
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
4. 核心原理详解
4.1 哈希冲突解决
链地址法
// 当发生哈希冲突时,将新节点添加到链表末尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
}
红黑树优化
当链表长度超过 TREEIFY_THRESHOLD(8) 且数组长度达到 MIN_TREEIFY_CAPACITY(64) 时,链表转换为红黑树:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
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;
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);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
4.2 扩容机制原理
扩容时机
- 元素数量超过阈值 (capacity × loadFactor)
- 链表长度超过阈值但数组长度小于 MIN_TREEIFY_CAPACITY
高效重新哈希
JDK 1.8 优化:节点在新数组中的位置要么保持不变,要么是原位置 + 原容量
// 判断节点位置变化的巧妙方法
if ((e.hash & oldCap) == 0) {
// 位置不变(低位链表)
} else {
// 位置 = 原位置 + 原容量(高位链表)
}
4.3 负载因子作用
负载因子 = 元素数量 / 数组长度
- 默认值 0.75:时间和空间的平衡点
- 较小值:减少哈希冲突,但浪费空间
- 较大值:节省空间,但增加哈希冲突
5. 性能分析
5.1 时间复杂度
操作 | 平均情况 | 最坏情况 |
---|---|---|
插入 | O(1) | O(log n) |
删除 | O(1) | O(log n) |
查找 | O(1) | O(log n) |
5.2 空间复杂度
- O(n):存储 n 个元素
6. 使用注意事项
6.1 正确实现 hashCode() 和 equals()
class KeyClass {
private final String id;
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
KeyClass key = (KeyClass) obj;
return Objects.equals(id, key.id);
}
}
6.2 线程安全问题
// 解决方案 1:使用 ConcurrentHashMap
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
// 解决方案 2:使用 Collections.synchronizedMap
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
// 解决方案 3:使用同步块
synchronized (map) {
// 操作 map
}
7. 总结
HashMap 的核心设计思想:
- 数组 + 链表/红黑树的复合结构平衡了空间和时间效率
- 动态扩容机制保证哈希表的负载均衡
- 红黑树优化解决了极端情况下的性能退化问题
- 位运算优化提升了计算效率
理解 HashMap 的源码和原理对于编写高性能的 Java 程序至关重要