HashMap一些关键知识点

你可能有误区的地方

Posted by bulingfeng on October 5, 2025

HashMap关键知识点总结

HashMap 作为一个 java 程序员天天都使用的类,但是如果被问一些关键问题的时候,自己好像知道,但是更多的是自己好像又说不明白怎么回事。而这些关键的知识点往往再某些时候变得非常重要,比如性能优化的时候。

下面我对以下几个方面来进行说明:

  1. 为何 table 的大小必须是 2 的幂
  2. 扩容
  3. 树化和反树化

为何table的大小必须是2的幂

目的 如果容量是任意数 如果容量是 2 的幂
取模性能 需要除法 % 位运算 &,快很多
分布均匀性 低位不一定分布好 通过高位扰动 + 全 1 掩码保证均匀
扩容迁移 需要重新计算所有 hash 只需看一位(hash & oldCap)
实现复杂度 简单、高效

hash & oldCap==0,则不一定,数据还停留在原来的位置。如果hash & oldCap!=0 则需要移动到原桶 + oldCap的位置。

扩容

hashmap 何时进行扩容呢?

当 hashmap 中的元素大于 负载因子*tablesize 的时候,这个时候就会触发扩容。并且扩容为上一次的 2 倍。

当然你也可以手动设置初始 tablesize 的大小,都会调用tableSizeFor,这个方法使不管你的初始的 tablesize 是否为 2 的幂,都会变成 2 的幂。

这里有个需要注意的点:

在 hashmap 放数据之前, threshold 其实就是等于 tablesize 的容量的,当进行第一次 put 的时候会调用 resize,这个时候才会 threshold=factory*capacity;

具体的内容如下

阶段 threshold 的意义 是否用 loadFactor 说明
构造函数 临时记录期望容量(经过 tableSizeFor) ❌ 没有 仅保证容量为 2 的幂
第一次 resize() 真正计算扩容阈值 ✅ 有 threshold = capacity × loadFactor
扩容之后 更新阈值 ✅ 有 newThreshold = newCapacity × loadFactor

树化和反树化

树化必须同时满足以下两个条件:

  1. 链表中的元素个数必须大于等于 8;
  2. tablesize 的大小必须大于等于 64;

树化的目的是为了平衡内存空间和查询效率;毕竟树的维护还是很重,所以当数据量少的时候要尽量避免树化。

反树化的过程,很多人写成了当节点小于 6 的时候就会反树化,其实准确的来说应是如下的逻辑

1
2
3
4
5
6
7
8
if (root == null
    || (movable
        && (root.right == null
            || (rl = root.left) == null
            || rl.left == null))) {
    tab[index] = first.untreeify(map);  // too small
    return;
}

root == null:树被删到空,直接退回链表(其实就是没有树了)。

movable && (...):当允许调整这个桶(movabletrue,正常删除场景)并且树已经“太小/太浅”*时,退回链表。 这里用的是*结构性检查而不是显式计数:

  • root.right == null:右子树都没了 → 过小
  • root.left == null:左子树都没了 → 过小
  • root.left.left == null:左子树深度不够(形如 4~5 个节点的瘦树)→ 过小

下面是常见的问题

问题 答案
默认初始容量是多少? 16
默认负载因子是多少? 0.75
扩容倍数? 2 倍
触发条件? size ≥ capacity × loadFactor
为什么是 2 的幂? 为了用 (n - 1) & hash 快速定位桶
扩容时会不会重新计算 hash? 不会,只根据旧容量位判断去低位桶或高位桶
扩容开销大吗? 是的,需要重新分配数组并迁移所有节点(O(n))