hashmap底层是长度为2的幂次的node数组,每个元素存链表头或红黑树根;链表转红黑树需同时满足链表长度≥8且数组长度≥64。

HashMap底层用的是数组,但不是普通数组
Java 8 的 HashMap 底层确实是数组,但这个数组的每个元素类型是 Node(或其子类),不是 Object 或泛型 V。也就是说,它不是「存值的数组」,而是「存链表/红黑树头节点的数组」。
数组长度永远是 2 的幂次(如 16、32、64),这是为了用位运算 (n - 1) & hash 替代取模 hash % n,加快下标计算——但前提是数组长度不能动态变,扩容必须重建。
- 初始化时默认容量是
16,负载因子0.75,所以第一次扩容触发点是第 13 个元素(16 * 0.75 = 12) - 如果构造时传了初始容量(比如
new HashMap(10)),实际会自动向上取最近的 2 的幂,变成16,不是 10 - 数组本身不存储 key-value 对,只存链表头或红黑树根;真正数据在
Node、TreeNode或TreeBin里
链表转红黑树的条件不止是“长度≥8”
光看链表长度 ≥ 8 不够,还得满足「当前桶所在数组长度 ≥ 64」。这是为了防止在小数组上过早树化——小数组扩容频繁,树化再退化开销反而大。
反过来说:即使某个桶里链表已经有 15 个节点,只要整个 table 长度还是 16 或 32,它也不会转成红黑树。
- 判断逻辑在
treeifyBin()方法里:if (tab == null || tab.length 就直接扩容,不树化 -
MIN_TREEIFY_CAPACITY固定为64,不可配置 - 红黑树节点是
TreeNode,但它不是直接继承Node,而是通过next字段维持原有链表结构,实现「树链共存」
get() 查找时,红黑树和链表走的是两套逻辑
查一个 key,先算 hash,定位到数组下标,然后根据该位置的节点类型分叉处理:
- 如果是普通
Node,就遍历链表,逐个比对hash和equals() - 如果是
TreeNode,就走红黑树的getTreeNode(),基于compareTo()或hashCode分治查找,平均时间复杂度 O(log n) - 注意:即使 key 实现了
Comparable,也未必走比较逻辑——只有当 key 类型一致且能比较时,才用compareTo();否则仍 fallback 到hashCode+equals()
所以别以为用了红黑树就一定更快:key 如果没实现 Comparable,或者大量 hash 冲突但 key 不可比较,树查找其实和链表差别不大,还多了树节点额外内存开销。
扩容时链表拆分不是简单平分,而是按 hash 的高位比特重分配
扩容从 16→32,不是把旧链表一分为二平均塞进两个新桶,而是看每个节点 hash 值的「第 5 位」(即新增的那一位)是 0 还是 1,决定它去新数组的 i 位还是 i + 16 位。
这个设计让同一个链表里的节点大概率被拆到两个不同桶,减少新桶继续冲突的概率,也避免 rehash 全量计算。
- 源码关键行:
e.hash & oldCap,其中oldCap是旧容量(如 16 → 二进制 10000),这个与操作结果非 0 即 oldCap - 因此扩容后,原
table[i]的链表最多生成两个子链表:loHead(高位为 0)和 hiHead(高位为 1) - 如果某次扩容后发现某个新桶又堆积了 8+ 个节点,且数组已 ≥64,才会再次触发树化
这个按位拆分逻辑,意味着如果你手动 set 了一堆 hash 值仅低位不同的 key(比如 "a"、"b"、"c"),它们可能长期挤在一个桶里,哪怕扩容多次——这是哈希函数质量差的真实代价,不是 HashMap 本身的问题。









