📦 第一章:HashMap 核心架构深度剖析
1.1 三层架构设计思想
整体架构
java
HashMap 对象
├── 字段区(管理状态)
│ ├── table: Node<K,V>[] // 核心数组
│ ├── size: int // 当前元素个数
│ ├── threshold: int // 扩容阈值 = capacity * loadFactor
│ ├── loadFactor: float // 负载因子(默认0.75)
│ └── modCount: int // 结构修改计数器
│
├── 数据结构层(存储实现)
│ ├── 第一层:Node<K,V>[] 数组(桶数组)
│ │ └── 每个元素称为一个"桶"(bucket)
│ ├── 第二层:链表(Node 通过 next 连接)
│ └── 第三层:红黑树(TreeNode 树形结构)
│
└── 算法层(业务逻辑)
├── 哈希算法:key.hashCode() ^ (h >>> 16)
├── 定位算法:index = (n-1) & hash
├── 扩容算法:resize() 策略
└── 树化算法:treeifyBin() 条件
1.2 桶(Bucket)的本质理解
什么是桶?
java
// 桶就是数组 table 的一个元素位置
Node<K,V>[] table = new Node[16]; // 创建了16个桶
// 访问桶:
Node<K,V> bucket = table[3]; // 这是桶3
// bucket 可能是:
// 1. null(空桶)
// 2. 单个 Node(无冲突)
// 3. Node 链表(有冲突)
// 4. TreeNode(红黑树根节点)
桶索引计算过程
java
// 详细步骤分解
public V put(K key, V value) {
// 步骤1:计算哈希值(扰动函数)
int hash = hash(key);
// 实际是:h = key.hashCode(); hash = h ^ (h >>> 16);
// 步骤2:计算桶索引
int n = table.length; // 比如 n=16
int index = (n - 1) & hash; // 比如 (16-1)=15, 15 & hash
// 步骤3:找到对应的桶
Node<K,V> first = table[index]; // 这就是桶的内容
// ... 后续操作
}
为什么用位运算 & 而不是取模 %?
java
// 前提:table.length 必须是2的幂(16, 32, 64...)
// 当 n 是2的幂时,n-1 的二进制是全1
// 例如:n=16 (10000), n-1=15 (01111)
// 位运算:(n-1) & hash
// 效果等价于:hash % n
// 但位运算性能比取模快很多
// 示例:
hash = 25 (二进制 11001)
n-1 = 15 (二进制 01111)
25 & 15 = 9 (二进制 01001)
25 % 16 = 9 // 结果相同!
1.3 链表与红黑树的转换机制
树化的完整条件判断
java
// 在 putVal() 方法中的树化逻辑
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 添加到链表尾部
p.next = newNode(hash, key, value, null);
// 条件1:链表长度是否≥8
// binCount 从0开始,所以当 binCount=7 时,链表已有8个节点
if (binCount >= TREEIFY_THRESHOLD - 1) { // TREEIFY_THRESHOLD=8
// 调用 treeifyBin,但这里不一定真的树化!
treeifyBin(tab, hash);
}
break;
}
// ... 查找现有key
}
// treeifyBin 方法内部:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n;
// 条件2:数组容量是否≥64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
// 容量<64,先扩容而不是树化
resize(); // 扩容可能减少链表长度
} else {
// 容量≥64,真正执行树化
// ... 将链表转为红黑树
}
}
红黑树退化为链表的条件
java
// 在 TreeNode.split() 方法中(扩容拆分时)
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
// ... 将红黑树拆分为两个链表
if (loHead != null) {
// 条件:拆分后的链表长度≤6
if (lc <= UNTREEIFY_THRESHOLD) { // UNTREEIFY_THRESHOLD=6
tab[index] = loHead.untreeify(map); // 树转链表
} else {
tab[index] = loHead; // 保持红黑树
}
}
}
为什么是8和6这两个数字?
java
/*
* 基于泊松分布的概率计算:
*
* 在理想哈希分布下,链表长度达到k的概率:
* P(k) = e^(-λ) * λ^k / k!
* 其中 λ = 0.5(默认负载因子0.75时的期望)
*
* 计算结果:
* 长度0: 0.60653066
* 长度1: 0.30326533
* 长度2: 0.07581633
* 长度3: 0.01263606
* 长度4: 0.00157952
* 长度5: 0.00015795
* 长度6: 0.00001316
* 长度7: 0.00000094
* 长度8: 0.00000006 ← 概率极低(千万分之6)
*
* 设计哲学:
* 1. 链表长度达到8的概率极低,树化是小概率事件的优化
* 2. 6和8之间有2的差距,避免频繁转换(抖动)
* 3. 树节点比普通节点占用更多内存,不轻易使用
*/
1.4 扩容机制深度解析
扩容触发条件
java
// 在 putVal() 方法最后
++modCount;
// 当前元素数量 > 扩容阈值
if (++size > threshold) {
resize(); // 执行扩容
}
// threshold 的计算:
// threshold = capacity * loadFactor
// 默认:16 * 0.75 = 12
// 当 size > 12 时触发扩容
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;
}
// 正常情况:容量翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) {
newThr = oldThr << 1; // 阈值也翻倍
}
}
// 第二部分:创建新数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 第三部分:数据迁移(JDK 8 优化核心)
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 {
// JDK 8 优化:一次判断,决定去向
Node<K,V> loHead = null, loTail = null; // 低位链表
Node<K,V> hiHead = null, hiTail = null; // 高位链表
do {
Node<K,V> next = e.next;
// 关键判断:(e.hash & oldCap) == 0
// oldCap 是2的幂(如16=10000),只有hash的某一位决定
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; // 新位置
}
}
}
}
}
return newTab;
}
扩容优化原理详解
java
/*
* 为什么 (e.hash & oldCap) == 0 能判断节点去向?
*
* 假设 oldCap = 16 (二进制 10000)
* newCap = 32 (二进制 100000)
*
* 旧索引计算:index = hash & (16-1) = hash & 1111
* 新索引计算:index = hash & (32-1) = hash & 11111
*
* 关键观察:新索引比旧索引多了一位(第5位)
* 如果 hash 的第5位是0:新索引 = 旧索引
* 如果 hash 的第5位是1:新索引 = 旧索引 + 16
*
* 而 hash 的第5位正是:hash & 10000(即 oldCap)
*
* 示例:
* hash = 25 (二进制 11001)
* oldCap = 16 (二进制 10000)
* 25 & 16 = 16 (不等于0) → 第5位是1
* 旧索引:25 & 15 = 9
* 新索引:25 & 31 = 25 = 9 + 16
*
* 这样不需要重新计算 hash,一次位运算即可!
*/
🚨 第二章:线程安全问题全解析
2.1 JDK 7 的死循环问题(必须理解)
问题重现代码逻辑
java
// JDK 7 的 transfer 方法(扩容数据迁移)
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j]; // 获取桶j的链表头
if (e != null) {
src[j] = null; // 清空旧桶
do {
Entry<K,V> next = e.next; // 步骤1:记录下一个节点
int i = indexFor(e.hash, newCapacity); // 计算新位置
// 头插法:将e插入新桶的链表头部
e.next = newTable[i]; // 步骤2:e指向新桶的当前头
newTable[i] = e; // 步骤3:更新新桶的头为e
e = next; // 步骤4:处理下一个节点
} while (e != null);
}
}
}
死循环产生的详细步骤
java
初始状态(两个线程都看到的状态):
桶0: A → B → null (链表)
新数组初始:所有桶都是null
线程T1执行:
1. e = A, next = B(记录B)
2. 计算A的新位置(假设还是桶0)
3. 执行头插法:A插入新桶0 → 新桶0: A → null
此时线程T1被挂起 ⏸️
线程T2执行完整扩容:
1. 处理A:next=B,A插入新桶0 → 新桶0: A → null
2. 处理B:next=null,计算B的新位置(假设还是桶0)
B插入新桶0头部 → 新桶0: B → A → null
扩容完成,状态变为:
新桶0: B → A → null (注意:A.next=null, B.next=A)
线程T1恢复执行 🔄
此时T1的本地变量:e=A, next=B(但这是旧的B!)
4. e = next = B(准备处理B)
5. 处理B:计算B的新位置(桶0)
6. 头插法:B.next = newTable[0](当前是A)
结果:B → A
7. newTable[0] = B
结果:新桶0: B → A(看起来正常?)
继续循环:
8. e = next(但next已经是null,因为B已经处理完了?)
等等!问题在这里!
实际上:e = next = B.next
B.next是什么?在线程T2扩容后,B.next = A
所以 e = A !又回到了A!
继续处理A(第二次):
9. next = e.next = A.next = null
10. 头插法:A插入新桶0头部 → A.next = B(当前头)
结果:A → B
11. newTable[0] = A
最终:A → B → A → B ... 循环链表形成!♻️
可视化死循环形成
java
初始: A → B → null
线程T2扩容后: B → A → null
↑______↓
线程T1再次插入A:A插入头部
A → B → A → B ...
↑_________↓
2.2 JDK 8 的改进与剩余问题
尾插法解决死循环
java
// JDK 8 使用尾插法保持顺序
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null) // 第一个节点
loHead = e;
else
loTail.next = e; // 尾插法:添加到链表尾部
loTail = e; // 更新尾指针
} else {
// 同样尾插法...
}
} while ((e = next) != null);
仍然存在的并发问题
问题1:size++ 非原子操作
java
// 源码中的问题点
if (++size > threshold) // 这不是原子操作!
resize();
// ++size 实际上分为三步:
// 1. 读取size的值到CPU寄存器
// 2. 寄存器值加1
// 3. 结果写回内存
// 并发场景:
// 线程T1:读取size=10
// 线程T2:读取size=10(同时读取)
// 线程T1:计算11,写回11
// 线程T2:计算11,写回11
// 结果:加了两个元素,但size只增加1
问题2:链表节点丢失
java
// putVal中的链表添加逻辑
if ((e = p.next) == null) {
// 这里不是原子操作!
p.next = newNode(hash, key, value, null);
// 并发场景:
// 线程T1:判断p.next == null(true)
// 线程T2:判断p.next == null(true),并执行 p.next = nodeB
// 线程T1:执行 p.next = nodeC
// 结果:nodeB丢失了! A → C,B不见了
}
问题3:可见性问题
java
// HashMap的字段没有用volatile修饰
transient Node<K,V>[] table; // 非volatile
transient int size; // 非volatile
// 一个线程修改后,另一个线程可能看不到最新值
// 因为Java内存模型(JMM)的 happens-before 关系不保证
2.3 安全使用指南
正确的多线程用法
java
// 方案1:使用ConcurrentHashMap(推荐)
ConcurrentHashMap<String, Object> safeMap = new ConcurrentHashMap<>();
// 方案2:使用Collections.synchronizedMap(包装器)
Map<String, Object> synchronizedMap =
Collections.synchronizedMap(new HashMap<>());
// 方案3:完全避免共享(ThreadLocal)
ThreadLocal<Map<String, Object>> threadLocalMap =
ThreadLocal.withInitial(HashMap::new);
// 方案4:只读副本(copy-on-write)
class CopyOnWriteMap<K, V> {
private volatile Map<K, V> map = new HashMap<>();
public void put(K key, V value) {
Map<K, V> newMap = new HashMap<>(map);
newMap.put(key, value);
map = newMap; // volatile写,保证可见性
}
public V get(K key) {
return map.get(key); // volatile读
}
}
🔄 第三章:各种Map实现对比
📊 3.1 Java Map 家族全景图
java
Map接口
├── AbstractMap(抽象基类)
│ ├── HashMap(最常用)
│ │ ├── LinkedHashMap(保持顺序)
│ │ └── WeakHashMap(弱引用)
│ ├── TreeMap(红黑树实现)
│ ├── EnumMap(枚举专用)
│ └── IdentityHashMap(==比较)
│
├── ConcurrentMap接口(并发)
│ ├── ConcurrentHashMap(并发哈希表)
│ ├── ConcurrentSkipListMap(并发跳表)
│ └── ConcurrentNavigableMap(并发导航)
│
├── SortedMap接口(排序)
│ ├── TreeMap(实现)
│ └── ConcurrentSkipListMap(实现)
│
└── NavigableMap接口(导航)
├── TreeMap(实现)
└── ConcurrentSkipListMap(实现)
🔄 3.2 所有Map实现详细对比
3.2.1 基本Map实现对比
| 特性 | HashMap | TreeMap | LinkedHashMap | IdentityHashMap | EnumMap | WeakHashMap |
|---|---|---|---|---|---|---|
| 底层结构 | 数组+链表/红黑树 | 红黑树 | 数组+链表/树+双向链表 | 线性探测数组 | 紧凑数组 | 数组+链表+弱引用 |
| 排序 | 无 | 按键自然/比较器排序 | 插入/访问顺序 | 无 | 枚举定义顺序 | 无 |
| null处理 | ✅允许key/value | ✅值允许null | ✅允许key/value | ✅允许key/value | ❌key不能为null | ✅允许key/value |
| 线程安全 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| 时间复杂度 | O(1)平均/O(log n)最坏 | O(log n) | O(1)平均/O(log n)最坏 | O(1)平均/O(n)最坏 | O(1) | O(1)平均/O(n)最坏 |
| 内存占用 | 中等 | 较大(树节点) | 较大(额外指针) | 较小(数组存储) | 最小 | 中等 |
| 迭代顺序 | 不确定(哈希决定) | 按键排序 | 插入或访问顺序 | 不确定 | 枚举定义顺序 | 不确定 |
| 特殊用途 | 通用哈希表 | 范围查询、排序 | LRU缓存、保持顺序 | 对象标识比较 | 枚举键值映射 | 缓存、防止内存泄漏 |
3.2.2 并发Map实现对比
| 特性 | ConcurrentHashMap | ConcurrentSkipListMap | Collections.synchronizedMap |
|---|---|---|---|
| 底层结构 | 数组+链表/红黑树+CAS | 跳表(Skip List) | 包装器+互斥锁 |
| 排序 | 无 | 按键自然/比较器排序 | 依赖被包装的Map |
| null处理 | ❌key/value不能为null | ❌key/value不能为null | 依赖被包装的Map |
| 线程安全 | ✅ | ✅ | ✅ |
| 并发级别 | 高(细粒度锁) | 高(无锁读) | 低(全局锁) |
| 锁粒度 | 桶级别(synchronized) | 无锁读,CAS写 | 整个Map(方法级锁) |
| 时间复杂度 | O(1)平均/O(log n)最坏 | O(log n)平均 | 同被包装Map |
| 内存占用 | 较大(额外volatile字段) | 较大(跳表多层) | 小(仅包装器开销) |
| 适用场景 | 高并发哈希表 | 并发排序、范围查询 | 简单同步需求 |
🌳 3.3 详细分析每个Map实现
3.3.1 HashMap(再次深度分析)
内部常量完整列表
java
public class HashMap<K,V> {
// 默认初始容量:16(必须是2的幂)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量:2的30次方
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;
// 序列化相关常量
private static final long serialVersionUID = 362498820763181265L;
// 扩容时的标记节点(ForwardingNode)哈希值
static final int MOVED = -1;
// 树根节点的哈希值
static final int TREEBIN = -2;
// 保留节点的哈希值
static final int RESERVED = -3;
}
3.3.2 TreeMap(红黑树实现)
核心数据结构
java
public class TreeMap<K,V> {
// 比较器:决定排序规则
private final Comparator<? super K> comparator;
// 红黑树根节点
private transient Entry<K,V> root;
// 元素数量
private transient int size = 0;
// 结构修改计数器
private transient int modCount = 0;
// 红黑树节点定义
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left; // 左子节点
Entry<K,V> right; // 右子节点
Entry<K,V> parent; // 父节点
boolean color = BLACK; // 节点颜色
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
}
// 红黑树颜色常量
private static final boolean RED = false;
private static final boolean BLACK = true;
}
红黑树特性
java
/*
* 红黑树必须满足的5个性质:
* 1. 每个节点要么是红色,要么是黑色
* 2. 根节点是黑色
* 3. 所有叶子节点(NIL)是黑色
* 4. 红色节点的两个子节点都是黑色(不能有连续红节点)
* 5. 从任一节点到其每个叶子节点的所有路径包含相同数量的黑色节点
*
* 这些性质保证了:树的高度最多是2log(n+1)
* 因此所有操作的时间复杂度都是O(log n)
*/
TreeMap特有方法
java
public class TreeMap<K,V> {
// 导航方法(SortedMap接口)
public K firstKey() { return getFirstEntry().key; } // 最小键
public K lastKey() { return getLastEntry().key; } // 最大键
// 导航方法(NavigableMap接口)
public Map.Entry<K,V> lowerEntry(K key) { ... } // 严格小于
public Map.Entry<K,V> floorEntry(K key) { ... } // 小于等于
public Map.Entry<K,V> higherEntry(K key) { ... } // 严格大于
public Map.Entry<K,V> ceilingEntry(K key) { ... } // 大于等于
// 范围视图
public SortedMap<K,V> subMap(K fromKey, K toKey) { ... } // 范围视图
public SortedMap<K,V> headMap(K toKey) { ... } // 头部视图
public SortedMap<K,V> tailMap(K fromKey) { ... } // 尾部视图
// 逆序视图
public NavigableMap<K,V> descendingMap() { ... } // 逆序视图
// 获取第一个/最后一个Entry
public Map.Entry<K,V> firstEntry() { ... }
public Map.Entry<K,V> lastEntry() { ... }
// 弹出最小/最大元素
public Map.Entry<K,V> pollFirstEntry() { ... }
public Map.Entry<K,V> pollLastEntry() { ... }
}
3.3.3 LinkedHashMap(保持顺序的HashMap)
双重链表结构
java
public class LinkedHashMap<K,V> extends HashMap<K,V> {
// 在HashMap.Node基础上添加双向链表指针
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 维护插入顺序或访问顺序
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
// 链表头尾指针
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
// 访问顺序标志
final boolean accessOrder; // true=访问顺序,false=插入顺序
// 关键回调方法(模板方法模式)
void afterNodeAccess(Node<K,V> p) { ... } // 访问后调整顺序
void afterNodeInsertion(boolean evict) { ... } // 插入后可能移除最老元素
void afterNodeRemoval(Node<K,V> p) { ... } // 移除后更新链表
}
LRU缓存实现原理
java
public class LRUCache<K,V> extends LinkedHashMap<K,V> {
private final int maxSize;
public LRUCache(int maxSize) {
// 关键:accessOrder = true
super(16, 0.75f, true);
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
// 当大小超过限制时,移除最久未访问的元素
return size() > maxSize;
}
}
3.3.4 Hashtable(遗留类,已过时)
与现代Map的差异
java
public class Hashtable<K,V> extends Dictionary<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 1. 继承Dictionary(已过时的抽象类)
// 2. 方法级同步(性能差)
public synchronized V put(K key, V value) { ... }
public synchronized V get(Object key) { ... }
// 3. 不允许null键值
public synchronized V put(K key, V value) {
if (value == null) {
throw new NullPointerException();
}
// ...
}
// 4. 扩容策略:2n+1(不是2的幂)
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 计算新容量
int newCapacity = (oldCapacity << 1) + 1; // 2n+1
// ...
}
// 5. 枚举遍历器(老式API)
public Enumeration<K> keys() {
return getEnumeration(KEYS);
}
}
3.3.5 IdentityHashMap(身份哈希表)
核心特点:==比较而非equals比较
设计原理
java
public class IdentityHashMap<K,V> {
// 特殊结构:Object[]交替存储key和value
// table[2*i] = key, table[2*i+1] = value
Object[] table;
// 使用System.identityHashCode(),不是对象的hashCode()
// 因为要用==比较,所以不能用对象的hashCode()
}
与HashMap的关键区别
java
String a = new String("Hello");
String b = new String("Hello"); // 内容相同,但不是同一个对象
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put(a, 1);
hashMap.put(b, 2); // 会覆盖第一个,因为equals()相同
System.out.println(hashMap.size()); // 输出:1
IdentityHashMap<String, Integer> identityMap = new IdentityHashMap<>();
identityMap.put(a, 1);
identityMap.put(b, 2); // 两个都保存,因为==不同
System.out.println(identityMap.size()); // 输出:2
内部机制详解
java
// 1. 哈希计算:使用System.identityHashCode()
private static int hash(Object x, int length) {
int h = System.identityHashCode(x); // 与默认Object.hashCode()相同
// 乘以127再位移,改善分布
return ((h << 1) - (h << 8)) & (length - 1);
}
// 2. 冲突解决:线性探测(开放地址法)
private int findIndex(Object key, Object[] tab) {
int len = tab.length;
int i = hash(key, len);
while (true) {
Object item = tab[i];
if (item == key) { // 关键:==比较!
return i; // 找到
}
if (item == null) { // 空槽
return ~i; // 返回负值表示未找到
}
i = nextKeyIndex(i, len); // 线性探测下一个
}
}
// 3. 添加元素
public V put(K key, V value) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
Object item;
while ((item = tab[i]) != null) {
if (item == k) { // ==找到相同key
@SuppressWarnings("unchecked")
V oldValue = (V) tab[i + 1];
tab[i + 1] = value;
return oldValue;
}
i = nextKeyIndex(i, len); // 冲突,找下一个
}
// 找到空槽
tab[i] = k;
tab[i + 1] = value;
if (++size >= threshold)
resize(len); // 扩容
return null;
}
使用场景
java
// 场景1:对象标识映射(监控系统)
class ObjectMonitor {
private IdentityHashMap<Object, String> objectRegistry =
new IdentityHashMap<>();
public void register(Object obj, String name) {
// 同一个对象实例只能注册一次
objectRegistry.putIfAbsent(obj, name);
}
public String getName(Object obj) {
// 必须是同一个对象实例
return objectRegistry.get(obj);
}
}
// 场景2:序列化/反序列化
class SerializationContext {
// 跟踪已处理的对象,防止循环引用
private IdentityHashMap<Object, Integer> processed =
new IdentityHashMap<>();
public boolean isProcessed(Object obj) {
return processed.containsKey(obj); // ==比较
}
}
// 场景3:缓存对象原始状态
class StateCache {
// 缓存对象的原始状态(必须是同一个对象)
private IdentityHashMap<Object, Object> originalStates =
new IdentityHashMap<>();
public void saveOriginal(Object obj) {
originalStates.put(obj, deepCopy(obj));
}
public boolean isModified(Object obj) {
Object original = originalStates.get(obj);
return !Objects.equals(obj, original);
}
}
3.3.6 EnumMap(枚举专用Map)
核心特点:为枚举优化的极致性能
设计原理
java
public class EnumMap<K extends Enum<K>, V> {
// 关键:使用枚举ordinal()直接作为数组索引
private final Class<K> keyType;
private K[] keyUniverse; // 所有枚举值
private Object[] vals; // 值数组
private int size = 0;
// 构造时必须指定枚举类型
public EnumMap(Class<K> keyType) {
this.keyType = keyType;
keyUniverse = getKeyUniverse(keyType); // 获取所有枚举常量
vals = new Object[keyUniverse.length]; // 按枚举数量创建数组
}
}
极致优化的实现
java
// 1. 获取:O(1)直接数组访问
public V get(Object key) {
return isValidKey(key) ?
unmaskNull(vals[((Enum<?>)key).ordinal()]) :
null;
}
// 2. 放入:O(1)直接数组访问
public V put(K key, V value) {
typeCheck(key); // 确保是正确枚举类型
int index = key.ordinal(); // 直接使用序号
Object oldValue = vals[index];
vals[index] = maskNull(value);
if (oldValue == null)
size++;
return unmaskNull(oldValue);
}
// 3. 是否包含key:O(1)
public boolean containsKey(Object key) {
return isValidKey(key) &&
vals[((Enum<?>)key).ordinal()] != null;
}
// 4. 遍历:按枚举声明顺序
public Set<Map.Entry<K,V>> entrySet() {
// 返回按枚举顺序的EntrySet
// 遍历时直接从keyUniverse按顺序取
}
内存优势分析
java
/*
* 假设枚举有7个值:
*
* EnumMap内存布局:
* vals数组长度 = 7
* 存储:vals[0], vals[1], ..., vals[6]
*
* HashMap内存布局(对比):
* 1. Node<K,V>[] table: 长度16(默认)
* 2. 7个Node对象:每个约32字节
* 3. 额外的链表指针等
*
* 内存节省:约70%
*
* 访问性能对比(100万次操作):
* EnumMap.get(): 8ms
* HashMap.get(): 45ms (慢5倍以上)
*/
使用场景
java
// 场景1:系统状态映射
enum SystemState { STARTING, RUNNING, STOPPING, STOPPED, ERROR }
class SystemMonitor {
// 每个状态对应的处理器
private EnumMap<SystemState, Runnable> stateHandlers =
new EnumMap<>(SystemState.class);
public SystemMonitor() {
// 初始化映射
stateHandlers.put(SystemState.STARTING, this::handleStarting);
stateHandlers.put(SystemState.RUNNING, this::handleRunning);
// ...
}
public void transitionTo(SystemState newState) {
Runnable handler = stateHandlers.get(newState);
if (handler != null) {
handler.run();
}
}
}
// 场景2:一周计划表
enum DayOfWeek { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }
class WeeklySchedule {
private EnumMap<DayOfWeek, List<Event>> schedule =
new EnumMap<>(DayOfWeek.class);
public void addEvent(DayOfWeek day, Event event) {
schedule.computeIfAbsent(day, k -> new ArrayList<>())
.add(event);
}
public void printSchedule() {
// 按枚举顺序打印(周一到周日)
for (DayOfWeek day : DayOfWeek.values()) {
List<Event> events = schedule.get(day);
System.out.println(day + ": " + events);
}
}
}
// 场景3:权限映射
enum Permission { READ, WRITE, EXECUTE, DELETE, ADMIN }
class UserPermissions {
private EnumMap<Permission, Boolean> permissions =
new EnumMap<>(Permission.class);
public UserPermissions() {
// 默认所有权限为false
for (Permission p : Permission.values()) {
permissions.put(p, false);
}
}
public boolean hasPermission(Permission p) {
return Boolean.TRUE.equals(permissions.get(p));
}
}
最佳实践
java
// 1. 始终在构造时指定枚举类型
EnumMap<Color, String> good = new EnumMap<>(Color.class); // ✅
EnumMap<Color, String> bad = new EnumMap<>(); // ❌ 编译错误
// 2. 利用枚举顺序
// EnumMap自动按枚举声明顺序迭代
for (Map.Entry<Color, String> entry : enumMap.entrySet()) {
// entry.getKey() 按Color枚举声明顺序
}
// 3. 与EnumSet配合使用
EnumSet<DayOfWeek> workDays = EnumSet.range(DayOfWeek.MONDAY, DayOfWeek.FRIDAY);
EnumMap<DayOfWeek, String> tasks = new EnumMap<>(DayOfWeek.class);
// 只为工作日设置任务
for (DayOfWeek day : workDays) {
tasks.put(day, "Work");
}
🎯 3.4 选择决策矩阵
根据需求选择Map的决策表
| 需求 | 首选 | 次选 | 避免 | 理由 |
|---|---|---|---|---|
| 通用键值存储 | HashMap | LinkedHashMap | Hashtable | 性能最佳,最通用 |
| 需要线程安全 | ConcurrentHashMap | Collections.synchronizedMap | Hashtable | 并发性能最好 |
| 需要排序 | TreeMap | ConcurrentSkipListMap | HashMap | 天然支持排序 |
| 需要插入顺序 | LinkedHashMap | - | HashMap | 专门为此设计 |
| LRU缓存 | LinkedHashMap(accessOrder=true) | - | HashMap | 内置移除最老元素机制 |
| 枚举键 | EnumMap | HashMap | TreeMap | 性能极致优化 |
| 对象标识(==) | IdentityHashMap | - | HashMap | 专门==比较 |
| 自动清理缓存 | WeakHashMap | SoftReference+HashMap | HashMap | 弱引用自动GC |
| 范围查询 | TreeMap | ConcurrentSkipListMap | HashMap | 支持subMap等操作 |
| 高并发排序 | ConcurrentSkipListMap | TreeMap+外部锁 | TreeMap | 并发安全且排序 |
| 配置存储 | Properties | HashMap | Hashtable | 专为.properties设计 |
| 简单同步 | Collections.synchronizedMap | Hashtable | HashMap | 简单包装即可 |
内存优化选择指南
java
// 内存敏感场景的选择
public class MemorySensitiveApplication {
// 场景1:大量小对象,内存紧张
// ❌ HashMap:每个Entry额外开销大
// ✅ EnumMap:如果key是枚举,内存最小
// ✅ 数组+线性探测:自定义实现
// 场景2:缓存系统,需要自动清理
// ✅ WeakHashMap:自动清理无引用key
// ✅ Guava Cache:更完整的缓存解决方案
// 场景3:固定键集合,性能要求高
// ✅ EnumMap:性能最好
// ✅ 数组映射:如果键是连续整数
// 场景4:需要顺序且内存有限
// ✅ LinkedHashMap:比TreeMap内存小
// ❌ TreeMap:每个节点额外指针多
}
🔍 3.5 源码学习重点
每个Map的核心学习点
1. HashMap
-
重点:数组+链表/红黑树转换逻辑
-
关键方法:putVal(), resize(), treeifyBin()
-
算法:扰动函数、扩容优化((e.hash & oldCap) == 0)
2. TreeMap
-
重点:红黑树插入、删除、旋转操作
-
关键方法:fixAfterInsertion(), fixAfterDeletion()
-
算法:红黑树平衡算法
3. ConcurrentHashMap
-
重点:CAS操作、锁分段、并发扩容
-
关键方法:putVal(), transfer()(扩容)
-
并发控制:sizeCtl字段、ForwardingNode
4. LinkedHashMap
-
重点:双向链表维护、访问顺序模式
-
关键方法:afterNodeAccess(), afterNodeInsertion()
-
设计模式:模板方法模式
5. EnumMap
-
重点:枚举ordinal()的直接使用
-
内存优化:单数组存储、无哈希冲突
6. IdentityHashMap
-
重点:线性探测开放地址法
-
特殊比较:System.identityHashCode()和==比较
7. WeakHashMap
-
重点:弱引用与ReferenceQueue
-
自动清理:expungeStaleEntries()机制
8. ConcurrentSkipListMap
-
重点:跳表数据结构、无锁读取
-
并发控制:CAS构建索引层
📝 第四章:常见问题与解决方案
HashMap常见问题
问题1:为什么重写equals()必须重写hashCode()?
java
class ProblemKey {
private String id;
@Override
public boolean equals(Object obj) {
// 只重写equals,没重写hashCode
// 问题:两个相等的对象可能有不同的hashCode
}
// 缺少hashCode()重写!
}
// 导致的问题:
HashMap<ProblemKey, String> map = new HashMap<>();
ProblemKey key1 = new ProblemKey("123");
ProblemKey key2 = new ProblemKey("123"); // equals返回true
map.put(key1, "value");
String value = map.get(key2); // 返回null!因为hashCode不同
// 解决方法:
@Override
public int hashCode() {
return Objects.hash(id); // 确保相等对象有相同hashCode
}
问题2:HashMap遍历时修改抛异常
java
Map<String, String> map = new HashMap<>();
map.put("A", "1");
map.put("B", "2");
// 错误:遍历时修改(除了通过iterator.remove())
for (String key : map.keySet()) {
if (key.equals("A")) {
map.remove(key); // 抛出ConcurrentModificationException!
}
}
// 正确方法1:使用迭代器的remove()
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
if (entry.getKey().equals("A")) {
it.remove(); // 安全移除
}
}
// 正确方法2:Java 8+
map.entrySet().removeIf(entry -> entry.getKey().equals("A"));
// 正确方法3:记录要删除的key,遍历后删除
List<String> keysToRemove = new ArrayList<>();
for (String key : map.keySet()) {
if (key.equals("A")) {
keysToRemove.add(key);
}
}
keysToRemove.forEach(map::remove);
问题3:HashMap内存泄漏
java
// 场景:使用可变对象作为key
class MutableKey {
private String value;
public MutableKey(String value) { this.value = value; }
public void setValue(String value) { this.value = value; }
@Override
public int hashCode() { return value.hashCode(); }
@Override
public boolean equals(Object obj) { /* 基于value比较 */ }
}
// 使用问题:
HashMap<MutableKey, String> map = new HashMap<>();
MutableKey key = new MutableKey("initial");
map.put(key, "value");
// 修改key的hashCode依赖的字段
key.setValue("changed"); // hashCode变了!
// 现在无法通过任何key获取到value了!
map.get(new MutableKey("initial")); // 找不到(hashCode不同)
map.get(key); // 也找不到(key在错误的桶里)
// 但key-value对还在map中,造成内存泄漏!
// 解决方案:
// 1. 使用不可变对象作为key(String、Integer等)
// 2. 如果必须用可变对象,修改后从map中移除再重新放入