本文主要结合 JDK8 之后的 HashMap 源码思想,系统讲清楚 Java 中 HashMap 的底层原理(数组 + 铉表 + 红黑树)。


# 一、HashMap 总体结构

JDK8 中 HashMap 的核心结构:

数组(Node<K,V>[] table)
        ↓
数组每个位置是一个链表
        ↓
链表过长转为红黑树

即:

数组 + 链表 + 红黑树

核心成员变量(JDK8):

transient Node<K,V>[] table;    // 哈希桶数组
transient int size;             // 元素个数
int threshold;                  // 扩容阈值
final float loadFactor;         // 负载因子

# 二、底层数据结构

# 1️⃣ Node 结构(源码精简)

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

本质就是一个 单向链表节点


# 2️⃣ 数组结构示意

table[0]
table[1] → Node → Node → Node
table[2]
table[3] → Node → Node (红黑树)

# 三、HashMap 的核心流程

我们按源码逻辑讲 4 个核心问题:

  1. hash 如何计算?
  2. put 如何插入?
  3. 为什么容量必须是 2 的幂?
  4. 扩容如何实现?

# 四、hash 计算原理

源码(JDK8):

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

# 为什么要右移 16 位异或?

因为:

数组下标计算 = (n - 1) & hash

如果只用低位,会导致高位信息浪费。

异或操作让:

高 16 位影响低 16 位

降低 hash 冲突。


# 五、数组下标如何确定?

int index = (n - 1) & hash;

假设:

n = 16
n-1 = 15 = 00001111

那么:

hash & 00001111

只取低 4 位。


# ⭐ 为什么容量必须是 2 的幂?

因为:

n-1 才会变成 111111 二进制

例如:

16 = 10000
15 = 01111

这样:

& 运算 = 等价于 % 16

效率远高于取模运算。


# 六、put () 插入流程(源码逻辑分析)

源码核心方法:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

核心逻辑(简化版):

if (table 为空)
    resize();
计算桶位置
如果该位置为空
    直接 new Node
否则
    如果 key 相同
        覆盖值
    否则
        遍历链表
            找到相同key就覆盖
            没找到就尾插
        如果链表长度 >= 8
            转红黑树

# 七、链表什么时候转红黑树?

源码条件:

if (binCount >= TREEIFY_THRESHOLD - 1) // 8
    treeifyBin(...)

# 触发条件:

条件
链表长度 ≥ 8才会考虑树化
数组长度 ≥ 64才真的树化

如果数组长度 < 64:

会优先扩容,而不是树化。

# 为什么?

因为:

冲突可能是容量太小,不是 hash 分布问题。


# 八、红黑树的作用

时间复杂度变化:

结构查找复杂度
链表O(n)
红黑树O(log n)

JDK8 优化目的:

防止恶意 hash 冲突攻击导致性能退化


# 九、resize 扩容机制(重点)

触发条件:

size > threshold

而:

threshold = capacity * loadFactor
默认 loadFactor = 0.75

# 扩容步骤

容量翻倍:

newCap = oldCap << 1

然后重新分布元素。


# ⭐ JDK8 的高效 rehash 算法(重点)

JDK7 做法:

重新计算 hash,重新取模

JDK8 优化:

只看 hash 的 新增那一位

if ((e.hash & oldCap) == 0)
    放在原位置
else
    放到 原位置 + oldCap

举例:

原容量 16 → 扩容到 32

只判断:

hash & 16

是 0 还是 1。


# 为什么可行?

因为容量翻倍 = 高位多一位

原索引:

(n-1) & hash

扩容后:

(2n-1) & hash

等于:

原索引 或 原索引 + oldCap

因此:

不需要重新算 hash,性能大幅提升


# 十、get () 查找流程

public V get(Object key)

步骤:

  1. 计算 hash
  2. 找到桶位置
  3. 如果是红黑树 → 树查找
  4. 如果是链表 → 顺序查找
  5. key.equals 比较

# 十一、HashMap 为什么线程不安全?

因为没有任何同步。

典型问题:

# 1️⃣ 多线程 resize 死循环(JDK7)

链表可能形成环。

JDK8 虽然优化了头插法问题,但依然不安全。


# 十二、HashMap 的时间复杂度

操作平均最坏
putO(1)O(log n)
getO(1)O(log n)

# 十三、JDK7 vs JDK8 区别

特性JDK7JDK8
数据结构数组 + 链表数组 + 链表 + 红黑树
插入方式头插法尾插法
扩容机制全量 rehash高位判断优化
并发风险可能死循环不会死循环

# 十四、完整执行流程总结

插入数据:

key → hash()
→ (n-1)&hash 定位数组
→ 桶为空 → new Node
→ 桶不为空 → 链表/树插入
→ size++
→ 判断是否扩容

# 十五、设计思想总结

HashMap 本质就是解决三件事:

  1. 如何减少 hash 冲突(扰动函数)
  2. 如何降低冲突代价(红黑树)
  3. 如何让扩容更高效(高位判断)

# 十六、一句话总结

HashMap = 通过 hash 定位数组桶,冲突用链表解决,链表长了转红黑树,容量翻倍用高位运算优化迁移。


如果你愿意,我可以再带你:

  • 从源码逐行演示一次 put 流程
  • 或手写一个简化版 HashMap
  • 或讲面试中 HashMap 高级考点

你想深入哪块?

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

HuaYu 微信支付

微信支付

HuaYu 支付宝

支付宝

HuaYu 贝宝

贝宝