应预估初始容量为大于预估元素数除以负载因子的最小2的幂(如800个元素、负载因子0.75,需设2048),合理调整负载因子,并确保key的hashcode稳定均匀,避免频繁扩容导致性能抖动。

HashMap 是 Java 中最常用的键值对存储结构,它的高效性依赖于哈希定位 + 动态扩容的双重机制。理解它怎么存、怎么找、什么时候扩、怎么扩,才能避免线上性能抖动和误用。
键值对怎么存进去的?
存一个 put(key, value) 不是简单塞进数组,而是分几步走:
- 先调用
key.hashCode()得到原始哈希值,再经过高位异或扰动(JDK8),降低低位重复带来的哈希冲突 - 用
(n - 1) & hash(n 是当前数组长度)快速算出该 key 应该落在哪个桶(数组索引),这比取模快得多 - 如果桶为空,直接新建 Node 放进去;如果不为空,就沿着链表逐个比较
equals()—— 注意:hashCode()相同只是可能冲突,最终靠equals()判定是否覆盖 - 当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树;若后续元素减少,树节点 ≤ 6 会退化回链表
扩容到底在扩什么?
扩容不是“加几个格子”,而是重建整个存储结构:
- 触发条件很明确:当 size > threshold(即元素个数超过“容量 × 负载因子”)时触发。默认初始容量 16,负载因子 0.75,所以第 13 个元素插入时就会扩容
- 新容量 = 原容量 × 2(比如 16 → 32),新 threshold = 新容量 × 0.75(32 × 0.75 = 24)
- 所有已有键值对必须重新计算桶位置——因为
n变了,(n - 1) & hash的结果大概率不同。这个过程叫 rehash,是扩容耗时的主因 - JDK8 对迁移做了优化:链表拆分时利用了 hash 的末位比特,能避免遍历两次;红黑树迁移也复用原有结构,不全量重建
为什么扩容后查找变快了?
根本目标是控制单个桶里的元素数量:
- 没扩容时,桶越多冲突越严重,链表/红黑树越长,查找从理想 O(1) 退化为 O(n) 或 O(log n)
- 扩容后桶数量翻倍,相同 hash 值的 key 更可能分散到不同桶里,平均链表长度下降,冲突概率降低
- 但注意:扩容本身是阻塞操作,多线程环境下未加同步会导致死循环(JDK7)或数据丢失(JDK8),所以高并发场景应优先考虑
ConcurrentHashMap
实战中怎么避免频繁扩容?
初始化时预估容量,比依赖默认值更靠谱:
- 如果确定要存 1000 个元素,按默认 0.75 负载因子,至少需要容量 ≥ 1000 / 0.75 ≈ 1334,向上取 2 的幂 → 2048
- 构造时直接写
new HashMap(2048),就能跳过多次扩容,节省时间和内存碎片 - 负载因子不是越小越好:设成 0.5 虽然更早扩容、冲突更少,但内存占用翻倍;设成 0.9 冲突风险高,查找慢——0.75 是时间与空间的平衡点
- 别用
putAll()批量塞大量数据前忘记检查容量,否则可能连续触发多次扩容
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











