说说 Java 中 HashMap 的原理?
- 标签:Java、Java 基础
🏛 业务架构师
HashMap 是业务代码里使用频率最高的容器,我作为架构师更关心的是什么时候用它才安全。原理上它是数组 + 链表 + 红黑树:元素按 hash 定位到桶,冲突时挂链表,链表长度到 8 且数组容量达 64 就树化为红黑树,把最坏查询从 O(n) 压到 O(logn);元素数超过容量 × 负载因子(0.75)就 2 倍扩容重新分布。
从业务落地角度我抓三点:
- 线程安全红线:秒杀库存扣减、实时在线人数这类并发读写场景绝不能用 HashMap,1.7 并发扩容会成环形链表死循环,1.8 也会丢数据,必须换 ConcurrentHashMap;
- 容量预估:能预估数据量就传初始容量(预期数 / 0.75 + 1),避免大促场景反复扩容造成接口毛刺;
- key 的设计:可变对象做 key 是埋雷,自定义 key 必须同时重写 hashCode 和 equals。
一句话总结:理解 HashMap 原理的价值在于让团队知道它的业务边界——单线程、读多写少、需要按 key 快速存取,越界就要换容器,而不是出了线上事故再补课。
🏗 系统架构师
我一般从"数据结构 + 哈希冲突处理 + 扩容"三层来讲 HashMap:底层是数组 + 链表 + 红黑树(JDK 1.8+),put 时先对 key 的 hashCode 做扰动(高 16 位异或低 16 位),再与 capacity-1 按位与定位桶下标;冲突时拉链,链表长度到 8 且数组容量 ≥ 64 时转红黑树,把最坏查询从 O(n) 压到 O(log n)。
工程视角的关注点:
- 负载因子 0.75、容量始终 2 的幂,扩容时元素要么留原位、要么移动"原下标 + oldCap",避免了重新计算 hash,这是 JDK 设计里很值得学的性能细节;
- 它非线程安全,多线程并发扩容在 1.7 里可能成环导致 CPU 打满,1.8 改成尾插解决了成环但仍会丢数据,所以并发场景我一定换 ConcurrentHashMap;
- 设计缓存或路由表时我会预估容量,
new HashMap<>(expected/0.75f + 1)一次性到位,避免线上高频扩容带来的毛刺。
💻 全栈开发专家
HashMap 底层是数组 + 链表 + 红黑树(JDK 8+)。put 时先对 key 的 hashCode 做扰动(h ^ (h >>> 16)),再按 (n - 1) & hash 定位桶;碰撞走链表,链表长度到 8 且数组长度 ≥ 64 就树化,节点删到 6 退回链表。元素超过 capacity * 0.75 就扩容一倍。
java
// 核心定位逻辑,等价于 hash % n,但位运算更快
int index = (n - 1) & (key.hashCode() ^ (key.hashCode() >>> 16));工程上的坑我踩过两个:
- 自定义对象做 key 必须同时重写
hashCode()和equals(),否则 get 不到;而且 key 要不可变,字段变了 hash 对不上,内存泄漏。 - 多线程并发 put 在 JDK 8 虽然不会死循环了(改成尾插),但仍会丢数据,并发场景直接换
ConcurrentHashMap,别侥幸。
排查 HashMap 相关问题时,我一般先看是不是 key 的 hashCode 质量差(大量碰撞),用 IDEA 的 debugger 看桶分布就能确认。