学习 Java 中 HashMap 的源码是深入理解哈希表数据结构、Java 集合框架设计思想的重要途径。下面从核心原理、关键属性、核心方法实现等方面,带你逐步剖析 JDK 8 及以上版本的 HashMap 源码(注:JDK 8 是 HashMap 实现的重要转折点,引入了红黑树优化,以下分析以 JDK 8 为基础)。
一、HashMap 核心原理概览
HashMap 是基于 哈希表 实现的 Map 接口,存储键值对(key-value),允许 key 和 value 为 null(key 仅允许一个 null),且无序(插入顺序与遍历顺序不一致)。
其核心思想是:
- 哈希函数 :通过 key的hashCode()计算哈希值,确定元素在数组中的存储位置(桶索引)。
- 解决哈希冲突 :当多个 key计算出相同的桶索引时,JDK 8 采用 链表 + 红黑树 结合的方式:- 当链表长度 <= 8 时,用链表存储(查询时间复杂度 O(n))。
- 当链表长度 > 8 时,转为红黑树(查询时间复杂度 O(log n))。
 
- 动态扩容 :当元素数量(size)超过 负载因子(loadFactor)× 数组长度(capacity) 时,触发扩容(数组长度翻倍),重新计算所有元素的存储位置(rehash)。
二、关键属性与常量
先看 HashMap 类中定义的核心属性和常量,理解其底层存储结构的基础:
            
            
              java
              
              
            
          
          public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 1. 常量
    // 默认初始容量(必须是 2 的幂):16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
    // 最大容量(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 时,即使链表长度超 8,也先扩容而非转树
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 2. 核心存储结构:哈希桶数组(数组 + 链表/红黑树)
    // 类型是 Node<K,V>[],Node 是链表节点;若转树则为 TreeNode(继承 Node)
    transient Node<K,V>[] table;
    // 3. 其他关键属性
    // 元素数量(key-value 对的个数)
    transient int size;
    // 结构修改次数(用于迭代器的快速失败机制)
    transient int modCount;
    // 扩容阈值(= capacity × loadFactor),当 size 超过此值时触发扩容
    int threshold;
    // 负载因子(可在构造方法中指定)
    final float loadFactor;
}- table数组 :- HashMap的核心存储容器,每个元素是一个- Node节点(链表节点)或- TreeNode节点(红黑树节点)。
- 容量(capacity) :table数组的长度,必须是 2 的幂(原因后面解释),默认 16。
- 负载因子(loadFactor):控制哈希表的疏密程度,默认 0.75(平衡空间和时间效率)。
- 阈值(threshold) :触发扩容的临界值,初始为 DEFAULT_INITIAL_CAPACITY × DEFAULT_LOAD_FACTOR = 12。
三、哈希函数与桶索引计算
HashMap 中,key 的存储位置(桶索引)由两步计算得出,目的是减少哈希冲突:
1. 计算 key 的哈希值(hash() 方法)
            
            
              java
              
              
            
          
          static final int hash(Object key) {
    int h;
    // 若 key 为 null,哈希值为 0;否则取 key 的 hashCode(),并将高 16 位与低 16 位异或
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}- 作用:将 key的hashCode()(32 位整数)的高 16 位与低 16 位进行异或,混合哈希值的高位和低位,减少因低位重复导致的哈希冲突(尤其在数组长度较小时,高位无法参与索引计算,通过异或让高位信息融入低位)。
2. 计算桶索引(i = (n - 1) & hash)
通过哈希值计算数组索引时,HashMap 没有用取模运算(hash % n),而是用:
            
            
              java
              
              
            
          
          int index = (table.length - 1) & hash;- 原因:当 n(数组长度)是 2 的幂时,n - 1的二进制是全 1(如n=16时,n-1=15即1111),此时(n-1) & hash等价于hash % n,但位运算效率更高。
- 这也解释了为什么 capacity必须是 2 的幂:保证索引计算的均匀性,避免某些位置永远无法被使用。
四、核心方法解析
1. 构造方法
HashMap 有三个主要构造方法,用于初始化容量和负载因子:
            
            
              java
              
              
            
          
          // 1. 无参构造:使用默认容量(16)和默认负载因子(0.75)
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // 其他属性默认初始化
}
// 2. 指定初始容量:使用默认负载因子
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 3. 指定初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    // 初始容量最大不超过 MAXIMUM_CAPACITY(2^30)
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    this.loadFactor = loadFactor;
    // 计算初始阈值:将 initialCapacity 向上调整为最接近的 2 的幂(如 initialCapacity=10 → 16)
    this.threshold = tableSizeFor(initialCapacity);
}
// 辅助方法:返回 >= 给定值的最小 2 的幂(用于初始化容量)
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}- 注意:table数组(哈希桶)并非在构造方法中初始化,而是在第一次插入元素时(put方法)才真正初始化(延迟初始化,节省空间)。
2. put() 方法(核心中的核心)
put 方法用于添加键值对,流程较复杂,可分为以下步骤:
            
            
              java
              
              
            
          
          public V put(K key, V value) {
    // 调用 putVal 方法,传入 key 的哈希值、key、value,其他参数默认
    return putVal(hash(key), key, value, false, true);
}
/**
 * 真正执行插入的核心方法
 * @param hash key 的哈希值
 * @param key 键
 * @param value 值
 * @param onlyIfAbsent 若为 true,则仅当 key 不存在时才插入(不覆盖已有值)
 * @param evict 用于 LinkedHashMap 的标志,HashMap 中无实际意义
 * @return 旧值(若 key 已存在)或 null
 */
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 未初始化(null 或长度 0),则初始化 table(resize())
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 步骤 2:计算桶索引 i = (n-1) & hash,若桶为空(p == null),直接新建节点放入
    if ((p = tab[i]) == null)
        tab[i] = newNode(hash, key, value, null);
    else { // 步骤 3:桶不为空(存在哈希冲突)
        Node<K,V> e; K k;
        // 3.1 若桶中第一个节点的 hash 和 key 与当前 key 相同,记录该节点(e = p)
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 3.2 若桶是红黑树(p 是 TreeNode),则调用树的插入方法
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 3.3 若桶是链表,遍历链表
        else {
            for (int binCount = 0; ; ++binCount) {
                // 3.3.1 若遍历到链表尾部(e = p.next == null),新建节点插入尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 若链表长度超过 TREEIFY_THRESHOLD(8),则尝试转红黑树(treeifyBin)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 因为从 0 开始计数
                        treeifyBin(tab, hash);
                    break;
                }
                // 3.3.2 若链表中存在相同 key,跳出循环(e 已记录该节点)
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e; // 移动到下一个节点
            }
        }
        // 步骤 4:若存在相同 key 的节点(e != null),则覆盖旧值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 空方法,供 LinkedHashMap 重写
            return oldValue;
        }
    }
    // 步骤 5:若插入了新节点,修改次数 +1,检查是否需要扩容
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict); // 空方法,供 LinkedHashMap 重写
    return null;
}put 方法核心流程总结:
- 若哈希桶数组未初始化,先调用 resize()初始化。
- 计算桶索引,若桶为空,直接插入新节点。
- 若桶不为空:
- 若第一个节点与当前 key相同,直接覆盖。
- 若桶是红黑树,调用树的插入方法。
- 若桶是链表,遍历链表:
- 若找到相同 key,覆盖旧值。
- 若未找到,插入链表尾部,若长度超 8 则尝试转红黑树(转树前会检查数组长度,若 <64 则先扩容)。
 
- 若找到相同 
 
- 若第一个节点与当前 
- 插入新节点后,若 size超过阈值,调用resize()扩容。
3. resize() 方法(扩容)
resize 是 HashMap 中最复杂的方法之一,负责初始化哈希桶或在容量不足时扩容(数组长度翻倍),并重新计算所有元素的存储位置(rehash)。
            
            
              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;
    // 情况 1:旧数组已初始化(oldCap > 0)
    if (oldCap > 0) {
        // 若旧容量已达最大值(2^30),则阈值设为 Integer.MAX_VALUE,不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 否则,新容量 = 旧容量 × 2(仍为 2 的幂),新阈值 = 旧阈值 × 2
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 情况 2:旧数组未初始化,但旧阈值 > 0(通常是通过带参构造方法指定了初始容量)
    else if (oldThr > 0)
        newCap = oldThr;
    // 情况 3:旧数组未初始化,且旧阈值 = 0(无参构造),使用默认值
    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;
    // 创建新的哈希桶数组(容量为 newCap)
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 若旧数组不为 null,将旧数组中的元素转移到新数组中
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null; // 帮助 GC
                // 若桶中只有一个节点,直接计算新索引并放入新数组
                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 { // preserve order
                    Node<K,V> loHead = null, loTail = null; // 索引不变的链表(low 链)
                    Node<K,V> hiHead = null, hiTail = null; // 索引 = 旧索引 + 旧容量的链表(high 链)
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 核心:通过 hash & oldCap 判断新索引(旧容量是 2 的幂,二进制只有一位为 1)
                        // 若结果为 0,新索引 = 旧索引;否则新索引 = 旧索引 + 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);
                    // 将 low 链放入新数组的旧索引位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 将 high 链放入新数组的(旧索引 + oldCap)位置
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}resize 核心逻辑:
- 扩容后容量为原来的 2 倍(保证仍是 2 的幂)。
- 转移旧元素到新数组时,利用 hash & oldCap判断新索引:- 若结果为 0,新索引 = 旧索引(low 链)。
- 若结果非 0,新索引 = 旧索引 + 旧容量(high 链)。
- 无需重新计算所有节点的哈希,效率更高。
 
- 红黑树在转移时可能拆分为两个链表或保持树结构(若节点数不足则转链表)。
4. get() 方法
get 方法用于根据 key 获取对应的 value,流程相对简单:
            
            
              java
              
              
            
          
          public V get(Object key) {
    Node<K,V> e;
    // 调用 getNode 方法,传入 key 的哈希值和 key
    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; // 未找到
}get 流程:
- 计算 key的哈希值和桶索引。
- 若桶为空,返回 null。
- 若桶不为空:
- 检查第一个节点是否匹配,匹配则返回。
- 若为红黑树,调用树的查找方法。
- 若为链表,遍历链表查找匹配节点。
 
五、红黑树相关(TreeNode)
JDK 8 中,TreeNode 继承自 Node,并实现了红黑树的结构(平衡二叉树的一种),用于优化长链表的查询效率。核心方法包括:
- putTreeVal:红黑树的插入。
- getTreeNode:红黑树的查找。
- split:扩容时红黑树的拆分。
- 红黑树的旋转(rotateLeft、rotateRight)和着色调整,保证树的平衡。
红黑树的实现较为复杂,核心是通过 节点颜色(红/黑) 和 旋转操作 维持树的平衡,确保查询、插入、删除的时间复杂度为 O(log n)。
六、总结与注意事项
- 线程不安全 :HashMap是非线程安全的,多线程环境下可能导致死循环(扩容时)或数据不一致,需使用ConcurrentHashMap替代。
- 迭代器快速失败 :modCount记录结构修改次数,迭代过程中若结构被修改,会抛出ConcurrentModificationException。
- key 的哈希值不可变 :若 key是自定义对象,需重写hashCode()和equals(),且保证key的哈希值在存储期间不变(否则可能无法找到元素)。
- 性能影响因素:初始容量和负载因子影响性能,容量过小会导致频繁扩容,过大则浪费空间;负载因子过高会增加哈希冲突概率,过低则空间利用率低。
通过阅读源码,不仅能理解 HashMap 的工作原理,还能学习到哈希表设计、冲突解决、动态扩容等经典算法思想,以及 Java 中的优化技巧(如位运算、延迟初始化等)。建议结合调试工具,跟踪 put、resize 等方法的执行过程,加深理解。