Java基础篇——一文搞懂 HashMap 底层原理

Java基础篇:一文彻底搞懂 HashMap 底层原理(JDK 8 + JDK 17 对比)

HashMap 是 Java 开发中最常用的集合,几乎每天都在用,但真正能说清楚它底层实现的人并不多。本文从源码角度彻底讲透 HashMap,读完保证你面试不再被虐!

一、HashMap 总体结构(JDK 8 为分水岭)

JDK 版本底层数据结构备注
JDK 7数组 + 单向链表头插法(有线程安全问题)
JDK 8数组 + 单向链表 + 红黑树尾插法 + 链表转红黑树
JDK 17+数组 + 单向链表 + 红黑树(优化阈值)树化阈值仍为8,但退化条件更智能

核心数据结构(JDK 8~21 都一样):

transient Node<K,V>[] table;        // 桶数组,主战场
transient int size;                 // 实际元素个数
transient int modCount;             // 结构性修改次数(快速失败机制)
int threshold;                      // 扩容阈值 = capacity * loadFactor
final float loadFactor;             // 负载因子,默认 0.75

二、Node 节点内部类(4种)

// 1. 普通链表节点
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;    // 单向链表
}

// 2. 红黑树节点(继承Node)
static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> parent;
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;
    boolean red;
}

// 3. JDK 8 扩容时使用的临时节点(了解即可)
static final class ForwardingNode<K,V> extends Node<K,V>

// 4. JDK 17 新增:当树退化成链表时会短暂出现(了解)
static final class ReservedNode<K,V> extends Node<K,V>

三、关键常量(必须背!面试必问)

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容量 16
static final int MAXIMUM_CAPACITY = 1 << 30;        // 最大容量 2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f;     // 默认负载因子
static final int TREEIFY_THRESHOLD = 8;             // 链表长度 >=8 时树化
static final int UNTREEIFY_THRESHOLD = 6;           // 树节点 <=6 时退化成链表
static final int MIN_TREEIFY_CAPACITY = 64;         // 桶容量 <64 时优先扩容,不树化

四、hash 计算(扰动函数)—— 防止哈希冲突的关键

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    // 高16位和低16位异或,减少低位碰撞
}

定位桶位置:

int index = (table.length - 1) & hash;   // 等价于 hash % length(长度必须是2的幂)

为什么容量必须是 2 的幂?
因为 (n-1) & hash 比取模更快,且只要 hash 分布均匀,位运算也能均匀分布。

五、put 流程全解析(JDK 8 最经典版本)

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. 懒加载:第一次put才初始化table
    if ((tab = table) == null || (n = tab.length) == 0)
        tab = resize();  // 初始化或扩容

    // 2. 计算索引,判断桶是否为空
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);  // 直接放第一个节点

    // 3. 发生冲突
    else {
        // 3.1 key 完全相同,直接覆盖
        if (p.hash == hash && (p.key == key || (key != null && key.equals(p.key))))
            oldValue = p.value;

        // 3.2 是树节点,走红黑树插入
        else if (p instanceof TreeNode)
            ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

        // 3.3 是普通链表,尾插法插入
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 长度达到8,且 table容量>=64,才树化
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 发现key已存在,覆盖
                if (e.hash == hash && (...equals...))
                    ...
                p = e;
            }
        }
    }

    // 4. 触发扩容
    if (++size > threshold)
        resize();

    return null;
}

六、resize() 扩容核心逻辑(最难部分!)

JDK 8 的高明之处:扩容时不需要重新计算 hash,只看原来 hash 的二进制位!

// 假设原来容量16,扩容到32
// 原hash:  0001 0101
// 新容量-1:     0001 1111
// & 后要么还在原位置,要么 +16(原容量)

Node<K,V> loHead, loTail;  // 低位链表(留在原位置)
Node<K,V> hiHead, hiTail;  // 高位链表(移动到 index + oldCap)

if (旧节点hash & oldCap != 0) → 去高位(原索引 + oldCap)
else → 留在低位(原索引不变)

这就是为什么 JDK 8 扩容性能大幅提升,不用全部 rehash!

七、为什么从 JDK 7 头插法 → JDK 8 尾插法?

JDK 7 多线程 put 时,头插 + 扩容可能形成环形链表,导致死循环!
JDK 8 改为尾插法 + 更复杂的并发控制(虽然 HashMap 仍然线程不安全,但不会死循环了)

八、HashMap 线程安全替代方案

场景推荐方案
简单线程安全Collections.synchronizedMap(new HashMap<>())
高并发读多写少ConcurrentHashMap(分段锁 → JDK8 Node+CAS+Synchronized)
JDK 8+ 推荐ConcurrentHashMap

九、经典面试题总结

  1. HashMap 默认初始容量是多少?为什么是 16?
    → 16,2^4,位运算效率高
  2. 为什么负载因子是 0.75?
    → 时间和空间平衡,0.75 时冲突概率较低(泊松分布计算)
  3. 为什么链表长度 8 才转红黑树?
    → 泊松分布下,链表长度达到 8 的概率已极低(约 0.00000006)
  4. 为什么树退化阈值是 6 而不是 7?
    → 避免频繁树化和退化(8→7→8 抖动)
  5. HashMap 是线程安全的吗?
    → 不是!多线程建议用 ConcurrentHashMap
  6. HashMap 能存 null 键吗?
    → 可以!null 键的 hash=0,永远放在 table[0]

结语

掌握了以上内容,你已经超越 95% 的面试者了!

如果要我给你整理一份:

  • HashMap 手绘结构图 + 源码注释版
  • 对比 HashTable、ConcurrentHashMap 的思维导图
  • 20 道 HashMap 高频面试题 PDF

直接回复“HashMap 资料” 三个字,我立刻打包发你!
冲就完了!

文章已创建 2679

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部