HashMap 底层原理全解析:一文带你理解透彻!

你以为的 put 和 get 很简单,其实背后暗藏玄机!本篇推文从源码视角深入剖析 HashMap,带你构建真正扎实的理解体系👇


一、底层结构:数组 + 链表(+ 红黑树)

  • HashMap 的核心是一个 Node[] table,也就是哈希桶数组;

  • 每个数组槽位存储的是一个链表或红黑树的 头节点

  • 当链表长度超过阈值(默认 8),且数组容量 ≥ 64,就会转为红黑树,优化查找性能。

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


二、核心操作解析:put、get

1. put():插入元素流程

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

核心步骤:

  1. 计算哈希值:扰动函数 + 位运算避免哈希碰撞;

  2. 定位桶位置:index = (n - 1) & hash;

  3. 判断桶是否为空:为空则直接插入,否则:

    遍历链表查找是否存在相同 key;

    如果超过 8 个节点就树化;

  4. 插入元素并可能触发 resize。

2. get():查找元素流程

public V get(Object key) {
    Node<K,V> e = getNode(hash(key), key);
    return e == null ? null : e.value;
}

核心步骤:

  • 根据 hash 值定位桶;

  • 遍历链表或红黑树比对 key(equals());

  • 返回对应的 value。


三、哈希扰动函数(JDK 1.8+)

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 利用高位参与运算,提高分布均匀性;

  • 减少 hash 冲突,提升性能。


四、扩容机制:容量翻倍 + 再哈希迁移

扩容条件:

if (size >= threshold) resize();

关键点:

  • 容量为 2 的幂,便于位运算取模;

  • 扩容时,每个元素要重新计算 index;

  • 链表不会倒置! 新旧桶位置与 hash 位模式相关;

  • 每次扩容成本高,建议提前初始化容量。


五、红黑树机制(树化)

JDK 1.8+ 优化:

  • 当某个桶中的链表长度 > 8 且数组容量 ≥ 64,会转为红黑树;

  • TreeNode 替代 Node,拥有左右子节点和颜色信息;

  • 查找、插入复杂度从 O(n) 优化到 O(log n)。

转换入口方法:

if (binCount >= TREEIFY_THRESHOLD - 1)
    treeifyBin(tab, hash);


六、线程安全问题

  1. HashMap 是 非线程安全 的;

  2. 多线程同时 put 可能导致死循环(链表环)、数据丢失;

  3. 并发环境应使用:ConcurrentHashMap或加锁封装后的Collections.synchronizedMap


七、JDK 1.7 vs 1.8 差异

特性

JDK 1.7

JDK 1.8

链表插入位置

头插法

尾插法(避免扩容死循环)

树化支持

不支持

支持红黑树

resize 机制

双重 for 循环 + rehash

hash 位与原容量比较直接判断位置

并发死循环风险

高(链表倒置 + 多线程)

降低(尾插 + 树化)


八、源码解读核心方法

putVal()的核心逻辑片段:​​​​​​​

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
else {
    // 遍历链表或树查找 key
    // 如果 key 已存在则覆盖,否则新增节点
}

resize():​​​​​​​

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
for (Node<K,V> e : oldTab) {
    // 按位迁移 old -> new
}

九、HashMap 适用建议

  • 不适合高并发写入场景;

  • 大量读取 + 少量更新场景性能优秀;

  • 支持 null key(唯一一个)和 null value;

  • 初始容量建议估算设置,减少扩容成本。


十、红黑树在 HashMap 中的结构源码

JDK 1.8 中引入红黑树是为了优化 hash 冲突严重时的查找性能。

当链表长度超过阈值(默认 8),HashMap 会将该桶的链表转化为红黑树。核心实现类是 TreeNode<K, V>:​​​​​​​

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;
}

  • parent、left、right:构建红黑树节点关系;

  • prev:保留双向链表结构(用于还原);

  • red:节点颜色标记,用于维护红黑树平衡;

树化入口方法

if (binCount >= TREEIFY_THRESHOLD - 1) 
    treeifyBin(tab, hash);

树化条件

  • 链表长度 ≥ 8;

  • 数组长度 ≥ 64,否则优先扩容;

查找时,采用 TreeNode.find() 递归二叉查找逻辑:

final TreeNode<K,V> find(int h, Object k, Class<?> kc)

结构复杂,查找复杂度降为 O(log n)。


十一、ConcurrentHashMap 和 HashMap 的底层设计对比

关键点

HashMap

ConcurrentHashMap (JDK 1.8+)

是否线程安全

❌ 否

✅ 是

实现结构

数组 + 链表 / 红黑树

数组 + 链表 / 红黑树

并发策略

分段锁 + CAS(synchronized + volatile)

put 实现方式

直接修改链表

synchronized + Unsafe CAS 更新

容量扩容

resize 会触发整体重哈希

多线程协作扩容

性能

单线程场景表现优秀

高并发写入场景稳定

允许 key/value 为 null

✅ 允许 null key 和 null value

❌ 禁止 key 和 value 为 null

结论:并发场景优先使用 ConcurrentHashMap,并可结合 LongAdder 做计数场景优化。


十二、自定义 key 的 hashCode 与 equals 方法最佳实践

自定义对象作为 HashMap 的 key 时,必须同时重写 hashCode() 和 equals() 方法,否则会导致 key 无法命中,出现“丢失”现象。

示例错误场景:

class User {
    String name;
    int age;
}

未重写 hashCode,以下操作将出现问题:

map.put(new User("Alice", 20), "A");
System.out.println(map.get(new User("Alice", 20))); // null

✅ 正确写法:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
    return Objects.hash(name, age);
}

📌 注意事项:

  • 建议使用 Objects.hash(...) 简洁表达;

  • 保证两个相同对象具有 相同的 hashCode 值与 equals 为 true

  • 不要让 hashCode() 随时间变化,否则会导致 key 命中失败。


十三、多线程 put 演示死循环现象(JDK 1.7 特性)

在 JDK 1.7 中,HashMap 的 resize() 采用头插法重建链表,多线程并发扩容时可能发生环形链表,造成死循环。

⚠️ 问题复现(JDK 1.7 下):

Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
    final int key = i;
    new Thread(() -> map.put(key, "value" + key)).start();
}

问题症状

  • CPU 100%;

  • 永远卡在 get() / put() 中无法返回;

  • 链表头插+rehash 导致环形链表。

🧪 原因:

  • resize() 中多线程操作 next 指针;

  • 指针串联错误,形成闭环结构;

  • get() 时遍历链表陷入死循环。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小健学 Java

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值