<think>我们正在分析HashMap的源码。根据引用内容,HashMap是Java中用于键值映射的高性能容器,基于哈希表实现,包含数组和链表(或红黑树)结构。我们将按照以下步骤分析源码:
1. 类声明与继承关系
2. 核心字段(如数组、负载因子等)
3. 构造方法
4. 关键方法(put, get, resize等)
5. 树化与反树化条件
注意:由于源码较长,我们只展示关键部分并解释。
根据引用[1],HashMap的类声明如下:
```java
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
```
引用[4]提到,HashMap的基本结构是一个数组和一组链表(或红黑树)。数组的每个桶(bucket)存储链表的头节点或红黑树的根节点。当链表长度超过阈值(默认为8)且数组长度达到64时,链表转换为红黑树;当树节点数小于6时,红黑树退化为链表。
下面我们逐步分析:
### 1. 核心字段
```java
// 默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树化阈值(链表长度大于等于8时可能树化)
static final int TREEIFY_THRESHOLD = 8;
// 树退化阈值(节点数小于等于6时退化)
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树化容量(数组长度至少达到64才树化)
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,长度总是2的幂
transient Node<K,V>[] table;
// 键值对集合
transient Set<Map.Entry<K,V>> entrySet;
// 元素个数
transient int size;
// 修改次数(用于迭代器的快速失败)
transient int modCount;
// 扩容阈值(容量*负载因子),当size超过threshold时扩容
int threshold;
// 负载因子
final float loadFactor;
```
### 2. 节点结构
链表节点(Node):
```java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// 构造方法等
// ...
}
```
树节点(TreeNode,继承自LinkedHashMap.Entry,而后者继承自Node):
```java
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. 构造方法
HashMap有多个构造方法,最常用的是指定初始容量和负载因子的:
```java
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
// 计算扩容阈值,注意此时数组还没有初始化,在第一次put时初始化
this.threshold = tableSizeFor(initialCapacity);
}
```
其中`tableSizeFor`方法返回大于等于给定容量的最小的2的幂次方数(如给定10,返回16)。
### 4. put方法
这是HashMap最核心的方法之一:
```java
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
```
其中`hash(key)`计算key的哈希值(高16位与低16位异或,减少哈希冲突):
```java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
```
`putVal`方法:
```java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果table为空,则通过resize()初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算桶下标:i = (n-1) & hash
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 桶为空,直接放入
else {
Node<K,V> e; K k;
// 判断桶中第一个节点是否与待插入节点相同(hash和key都相同)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果是树节点,则调用树节点的putTreeVal方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
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;
}
// 找到相同key的节点,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果存在相同key,则更新value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 回调(用于LinkedHashMap)
return oldValue;
}
}
++modCount;
// 如果size超过阈值,则扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 回调
return null;
}
```
### 5. 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) // 初始化时指定了初始容量
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 { // 链表节点
// 将链表拆分成两个链表:一个在原位置,一个在原位置+oldCap
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 判断节点在扩容后属于低位(原位置)还是高位(原位置+oldCap)
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;
}
```
### 6. 树化方法
当链表长度达到8且数组长度达到64时,进行树化:
```java
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 {
// 将Node节点替换为TreeNode节点
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);
}
}
```
### 总结
HashMap通过数组+链表/红黑树实现高效的键值存储。在插入元素时,通过哈希函数计算桶位置,解决冲突的方式是链表法,当链表过长时转换为红黑树以提高效率。当元素数量超过阈值(容量×负载因子)时,进行扩容(翻倍),并重新映射所有元素。
§§相关问题§§
1. HashMap的负载因子为什么是0.75?
2. 为什么HashMap的容量总是2的幂?
3. 在HashMap中,当链表长度超过8时为什么选择转化为红黑树而不是其他数据结构?
4. HashMap在并发环境下会出现什么问题?
5. HashMap的key为null时存储在什么位置?</think>### HashMap 源码分析
HashMap 是 Java 中基于哈希表的 `Map` 接口实现,用于存储键值对(key-value)。以下是其核心实现原理和源码关键逻辑分析:
---
#### **1. 底层数据结构**
HashMap 由 **数组 + 链表/红黑树** 组成(JDK 1.8+):
- **数组(桶)**:`Node<K,V>[] table`,初始长度 16(2 的幂)。
- **链表**:解决哈希冲突,节点为 `Node` 类(含 `hash, key, value, next` 字段)。
- **红黑树**:当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树(`TreeNode` 类),提高查询效率[^5]。
```java
// Node 类定义(链表节点)
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; // 指向下一个节点
}
```
---
#### **2. 核心参数**
```java
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量 16
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子
static final int TREEIFY_THRESHOLD = 8; // 链表转红黑树的阈值
static final int MIN_TREEIFY_CAPACITY = 64; // 最小树化容量
transient Node<K,V>[] table; // 存储桶的数组
int threshold; // 扩容阈值 = capacity * loadFactor
```
---
#### **3. 哈希计算与索引定位**
1. **计算哈希值**:
通过 `hash(key)` 将高 16 位与低 16 位异或,减少冲突。
```java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
```
2. **计算桶下标**:
`index = (table.length - 1) & hash`(等价于 `hash % length`,但效率更高)。
---
#### **4. 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 为空,则初始化(resize())
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤2:计算桶下标 i,若桶为空则直接插入新节点
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:若节点是红黑树,调用树插入方法
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);
// 链表长度≥8时尝试树化
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 发现 key 相同的节点,跳出循环
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 步骤6:更新已存在的 key 的值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
}
// 步骤7:检查扩容
if (++size > threshold)
resize();
return null;
}
```
---
#### **5. 扩容机制(resize)**
当元素数量超过 `threshold` 时,容量翻倍(`newCap = oldCap << 1`),并重新分配节点:
1. 创建新数组(长度为原数组的 2 倍)。
2. 遍历旧数组的每个桶:
- 若桶内只有一个节点:直接迁移到新位置。
- 若桶内是红黑树:调用 `split()` 方法拆分。
- 若桶内是链表:将链表拆分为 **低位链表**(原下标)和 **高位链表**(原下标 + oldCap),再放入新数组[^5]。
```java
// 扩容后节点新位置的计算逻辑
if ((e.hash & oldCap) == 0) {
// 放入低位链表(原下标)
} else {
// 放入高位链表(原下标 + oldCap)
}
```
---
#### **6. 线程安全性**
HashMap **非线程安全**,并发场景下可能引发问题:
- **死循环**:JDK 1.7 头插法扩容时可能形成环形链表。
- **数据覆盖**:多线程同时 put 时可能导致数据丢失。
解决方案:使用 `ConcurrentHashMap` 或 `Collections.synchronizedMap()`。
---
#### **7. 关键设计思想**
1. **容量为 2 的幂**:使 `(n-1) & hash` 等效于取模运算,且位运算更快。
2. **负载因子 0.75**:平衡时间与空间开销(过小则频繁扩容,过大则冲突增加)。
3. **红黑树优化**:解决链表过长时查询效率低的问题($O(n) \to O(\log n)$)。
---
### 总结
HashMap 通过哈希函数快速定位桶,结合链表/红黑树解决冲突,在扩容时重新分布节点以保持高效性。其设计在时间与空间效率上达到了较好平衡,是 Java 最常用的数据结构之一。