数据结构深度解析:Java Map 家族完全指南

📦 第一章: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中移除再重新放入
相关推荐
蓝眸少年CY2 小时前
测试Java性能
java·开发语言·python
秃了也弱了。2 小时前
python监听文件变化:Watchdog库
开发语言·python
一路往蓝-Anbo2 小时前
C语言从句柄到对象 (五) —— 虚函数表 (V-Table) 与 RAM 的救赎
c语言·开发语言·stm32·单片机·物联网
古译汉书2 小时前
keil编译错误:Error: Flash Download failed
开发语言·数据结构·stm32·单片机·嵌入式硬件
Bruce_kaizy2 小时前
2025年年度总结!!!!!!!!!!!!!!!!!!!!!!!!!!!
开发语言·c++
聆风吟º2 小时前
【顺序表习题|图解|双指针】合并两个有序数组 + 训练计划 I
c语言·数据结构·c++·经验分享·算法
linsa_pursuer2 小时前
最长连续序列
java·数据结构·算法·leetcode
强子感冒了2 小时前
Java集合框架深度学习:从Iterable到ArrayList的完整继承体系
java·笔记·学习
drebander2 小时前
Cursor IDE 中 Java 项目无法跳转到方法定义问题解决方案
java·ide·cursor