深入解析HashMap核心机制(底层数据结构及扩容机制详解剖析)

HashMap是Java集合框架中最常用的Map实现,其设计精妙且性能优越。下面我将从数据结构、哈希算法、扩容机制、源码分析四个维度为你全面解析。


一、HashMap底层数据结构演进

📊 JDK 1.7 vs JDK 1.8 对比

特性 JDK 1.7 JDK 1.8
底层结构 数组 + 链表 数组 + 链表 + 红黑树
链表插入方式 头插法 尾插法
扩容时链表处理 重新计算hash 保持原有顺序(高低位分离)
并发问题 头插法导致死循环 尾插法避免死循环
性能优化 链表转红黑树(阈值8)

🏗️ JDK 1.8 底层数据结构详解

java 复制代码
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
    // 核心字段
    transient Node<K,V>[] table;          // 底层数组(哈希桶数组)
    transient int size;                    // 实际元素个数
    int threshold;                         // 扩容阈值(capacity * loadFactor)
    final float loadFactor;                // 负载因子(默认0.75)
    transient int modCount;                // 修改次数(fail-fast机制)
    
    // 链表节点
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;        // key的hash值
        final K key;           // 键
        V value;               // 值
        Node<K,V> next;        // 指向下一个节点(链表)
    }
    
    // 红黑树节点(继承自LinkedHashMap.Entry)
    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;           // 颜色(红/黑)
    }
}

📐 数据结构图示

java 复制代码
        +-----------------------------------+
        |       哈希桶数组 (table)          |
        +-----------------------------------+
        | index | 内容                       |
        +-------+----------------------------+
        |  0    |  null                      |
        |  1    |  Node(key1, value1)        |
        |  2    |  TreeNode(root)            | ← 红黑树(链表长度≥8)
        |       |    /        \              |
        |       |   T1        T2             |
        |  3    |  Node → Node → Node        | ← 链表(长度<8)
        |  4    |  null                      |
        |  ...  |  ...                       |
        +-------+----------------------------+

核心设计

  • 数组:快速定位桶位置(时间复杂度O(1))
  • 链表:处理哈希冲突(同一桶位置的多个元素)
  • 红黑树:当链表过长时转换,提升查询性能(时间复杂度O(log n))

二、哈希算法与索引计算

🔢 哈希值计算流程

java 复制代码
// 1. 计算key的hashCode
int h = key.hashCode();

// 2. 扰动函数:高位参与运算,减少哈希冲突
h = h ^ (h >>> 16);

// 3. 计算数组索引(与运算代替取模,性能更高)
int index = h & (table.length - 1);

📊 扰动函数详解

java 复制代码
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

为什么需要扰动函数?

场景 无扰动函数 有扰动函数
哈希值 1111 1111 1111 1111 0000 0000 0000 0001 同左
数组长度-1 0000 0000 0000 0000 0000 1111 1111 1111 (1023) 同左
与运算结果 0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0000 0000 0000 1111 1111 1110
问题 仅低位参与,冲突率高 高位也参与,分布更均匀

扰动函数的作用:将高位的特征"扰动"到低位,使哈希值分布更均匀,减少冲突。


🧪 哈希冲突示例

java 复制代码
public class HashCollisionDemo {
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>();
        
        // 三个字符串的hashCode经过扰动后,可能落在同一桶
        String key1 = "Aa";
        String key2 = "BB";
        String key3 = "C#";
        
        System.out.println("key1 hashCode: " + key1.hashCode() + ", hash: " + hash(key1));
        System.out.println("key2 hashCode: " + key2.hashCode() + ", hash: " + hash(key2));
        System.out.println("key3 hashCode: " + key3.hashCode() + ", hash: " + hash(key3));
        
        map.put(key1, "Value1");
        map.put(key2, "Value2");
        map.put(key3, "Value3");
        
        // 这三个key可能落在同一个桶,形成链表
    }
    
    static int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
}

三、put()方法完整流程(含时序图)

📊 put()方法时序图

复制代码

🔍 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为空或长度为0,调用resize()初始化
    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. 检查第一个节点是否是目标key
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4. 如果是红黑树节点,调用红黑树的put方法
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5. 遍历链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    // 尾插法添加新节点
                    p.next = newNode(hash, key, value, null);
                    
                    // 检查是否需要转红黑树(阈值8)
                    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;
            }
        }
        
        // 6. 如果找到相同key,替换value
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);  // LinkedHashMap回调
            return oldValue;
        }
    }
    
    // 7. 修改次数+1(fail-fast)
    ++modCount;
    
    // 8. 检查是否需要扩容
    if (++size > threshold)
        resize();
    
    afterNodeInsertion(evict);  // LinkedHashMap回调
    return null;
}

四、扩容机制(resize)详解

📊 扩容时序图

复制代码

🔍 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) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 首次扩容:容量16,阈值12(16 * 0.75)
        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;  // 释放引用,便于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 {
                    // 高低位分离
                    Node<K,V> loHead = null, loTail = null;  // 低位链表(原索引)
                    Node<K,V> hiHead = null, hiTail = null;  // 高位链表(原索引+oldCap)
                    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 {
                            // 高位:索引 + 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;  // 原索引 + oldCap
                    }
                }
            }
        }
    }
    return newTab;
}

📐 高低位分离原理图解

假设:

  • 旧容量:16(二进制 0001 0000
  • 新容量:32(二进制 0010 0000
  • 哈希值:0101 0101
java 复制代码
旧索引计算:hash & (16-1) = 0101 0101 & 0000 1111 = 0101 (5)

扩容后索引计算有两种可能:
1. 保持原索引:0101 0101 & 0001 1111 = 0101 (5)
2. 原索引 + oldCap:0101 0101 & 0001 1111 = 10101 (21)

判断条件:hash & oldCap
- 如果 (hash & 16) == 0 → 保持原索引(低位)
- 如果 (hash & 16) != 0 → 原索引 + 16(高位)

示例:
hash1 = 0000 0101 → & 0001 0000 = 0 → 低位 → 索引5
hash2 = 0001 0101 → & 0001 0000 = 1 → 高位 → 索引21

优势

  • 无需重新计算hash
  • 保持链表原有顺序(避免JDK 1.7头插法的死循环问题)
  • 时间复杂度O(n),每个节点只需判断一次

五、链表转红黑树机制

📊 转换阈值

操作 阈值 说明
链表→红黑树 8 链表长度≥8且数组长度≥64
红黑树→链表 6 红黑树节点数≤6
java 复制代码
// 阈值常量
static final int TREEIFY_THRESHOLD = 8;   // 链表转树阈值
static final int UNTREEIFY_THRESHOLD = 6; // 树转链表阈值
static final int MIN_TREEIFY_CAPACITY = 64; // 转树最小数组容量

🔍 链表转红黑树流程

java 复制代码
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    
    // 优化:如果数组长度小于64,优先扩容而不是转树
    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;
        
        // 1. 将链表节点转换为TreeNode
        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);
        
        // 2. 构建红黑树
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

📐 红黑树结构示例

java 复制代码
        桶索引:5
        +-------------------+
        |   红黑树根节点    |
        |      (key50)      |
        +---------+---------+
                  |
        +---------+---------+
        |                   |
    (key30)红           (key70)黑
      /   \               /   \
  (key20) (key40)   (key60) (key80)

红黑树特性

  • 查询时间复杂度:O(log n)
  • 插入/删除时间复杂度:O(log n)
  • 相比链表(O(n)),在数据量大时性能显著提升

六、get()方法流程

java 复制代码
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;
    
    // 1. 检查table是否为空,桶是否为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        
        // 2. 检查第一个节点
        if (first.hash == hash && 
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        
        // 3. 遍历后续节点
        if ((e = first.next) != null) {
            // 3.1 红黑树
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            
            // 3.2 链表
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

七、性能分析与最佳实践

负载因子影响

java 复制代码
// 负载因子对比
HashMap<String, String> map1 = new HashMap<>(16, 0.5f);  // 容量8时扩容
HashMap<String, String> map2 = new HashMap<>(16, 0.75f); // 容量12时扩容(默认)
HashMap<String, String> map3 = new HashMap<>(16, 1.0f);  // 容量16时扩容
负载因子 扩容频率 空间利用率 哈希冲突概率
0.5
0.75 中(默认推荐)
1.0

✅ 最佳实践

1. 预估容量,避免频繁扩容
java 复制代码
// ❌ 不推荐:未知容量,频繁扩容
Map<String, User> userMap = new HashMap<>();
for (User user : userList) {
    userMap.put(user.getId(), user);
}

// ✅ 推荐:预估容量
// 公式:expectedSize / loadFactor + 1
int initialCapacity = (int) (userList.size() / 0.75f) + 1;
Map<String, User> userMap = new HashMap<>(initialCapacity);
for (User user : userList) {
    userMap.put(user.getId(), user);
}
2. 使用合适的负载因子
java 复制代码
// 空间敏感场景:降低负载因子
Map<String, String> cache = new HashMap<>(1024, 0.5f);

// 查询频繁场景:保持默认0.75
Map<String, String> config = new HashMap<>();
3. 避免使用可变对象作为key
java 复制代码
// ❌ 危险:修改key的hashCode会导致无法找到
class BadKey {
    private String name;
    // 有setter方法,hashCode可能改变
}

// ✅ 推荐:使用不可变对象
class GoodKey {
    private final String id;
    // 无setter,hashCode不变
}
4. 重写equals和hashCode
java 复制代码
public class User {
    private String id;
    private String name;
    
    @Override
    public int hashCode() {
        return Objects.hash(id);  // 只用id计算
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof User)) return false;
        User other = (User) obj;
        return Objects.equals(id, other.id);
    }
}

八、HashMap vs Hashtable vs ConcurrentHashMap

特性 HashMap Hashtable ConcurrentHashMap
线程安全 ❌ 否 ✅ 是(synchronized) ✅ 是(CAS + synchronized)
null键/值 ✅ 支持 ❌ 不支持 ✅ 支持(JDK 1.8)
底层结构 数组+链表+红黑树 数组+链表 数组+链表+红黑树
扩容机制 2倍扩容 2倍扩容 2倍扩容
性能 低(全表锁) 高(分段锁/CAS)
推荐使用 单线程 不推荐 多线程

九、总结:核心要点

🔑 核心机制总结

机制 关键点 作用
哈希算法 扰动函数 h ^ (h >>> 16) 减少哈希冲突
索引计算 (n-1) & hash 快速定位桶位置
扩容机制 2倍扩容 + 高低位分离 保持链表顺序,避免死循环
链表转树 长度≥8且容量≥64 提升查询性能
负载因子 默认0.75 平衡空间与时间
相关推荐
香芋Yu2 小时前
【大模型面试突击】03_大模型架构演进与对比
面试·职场和发展·架构
ruxshui2 小时前
元数据及元数据备份、迁移详解
数据库
禅与计算机程序设计艺术2 小时前
了解NoSQL的数据仓库和ETL
数据库·数据仓库·nosql·etl
专注前端30年2 小时前
负载均衡实战项目搭建指南:从基础到高可用全流程
运维·数据库·负载均衡
##学无止境##2 小时前
从0到1吃透Java负载均衡:原理与算法大揭秘
java·开发语言·负载均衡
梵得儿SHI2 小时前
Spring Cloud 核心组件精讲:负载均衡深度对比 Spring Cloud LoadBalancer vs Ribbon(原理 + 策略配置 + 性能优化)
java·spring cloud·微服务·负载均衡·架构原理·对比单体与微服务架构·springcloud核心组件
Desirediscipline3 小时前
#define _CRT_SECURE_NO_WARNINGS 1
开发语言·数据结构·c++·算法·c#·github·visual studio
知识即是力量ol3 小时前
多线程并发篇(八股)
java·开发语言·八股·多线程并发