HashMap的put方法先调用hash(key)扰动计算hash值:null键hash为0,非null键用hashCode()高16位异或低16位;再通过(n-1)&hash定位索引,要求n为2的幂;冲突时遍历链表/红黑树,满足长度≥8且数组≥64才树化;超阈值触发扩容,全量rehash;key的hashCode与equals必须一致,禁用可变对象作key。

put方法执行时如何计算key的hash值
HashMap的put方法第一步不是直接插入,而是调用hash(key)对key做扰动运算。这个静态方法会把key的hashCode()高16位异或低16位,目的是让高位也参与寻址,缓解低位碰撞——尤其当数组长度是2的幂次时,只取低位做下标容易导致大量key落在同一桶里。
注意:null key被特殊处理,它的hash值固定为0,且总被放在数组索引0的位置。
如何确定元素存入哪个数组槽位(index)
算出hash值后,真正定位下标用的是位运算:(n - 1) & hash,其中n是table数组长度(必须是2的幂)。这等价于取模hash % n,但更快。前提是数组长度必须是2的幂,否则位运算结果不等于取模,会导致索引越界或分布不均。
常见坑:
立即学习“Java免费学习笔记(深入)”;
- 手动传入初始容量如
new HashMap(10),实际扩容后数组长度变成16(向上取最近2的幂),不是10 - 如果重写了
hashCode()但没重写equals(),可能造成逻辑错误:两个equal对象hash不同,被当作不同key存两次
发生哈希冲突后怎么处理链表与红黑树
当目标槽位(bucket)已有节点,HashMap会遍历该位置的链表或红黑树:
- 先用
==比引用,再用equals()比内容,找到相同key就覆盖value - 没找到则新节点插在链表尾部(JDK 8+,之前是头插,有并发死链风险)
- 当链表长度≥8 且 数组长度≥64,链表转为红黑树;反之若数组太小,优先扩容而不是树化
- 红黑树节点数≤6时,会退化回链表
所以“链表转树”不是单看长度,要同时满足两个条件,否则只是扩容。
扩容机制如何触发与影响性能
当size > threshold(即capacity × loadFactor)时触发扩容。默认初始容量16、负载因子0.75,所以第13个元素put时就会扩容到32。
扩容过程是全量rehash:新数组长度翻倍,所有旧节点重新计算index并迁移。这是put最耗时的部分,尤其在频繁put且未预估容量时,可能连续多次扩容。
建议:
- 如果知道大概元素数量,初始化时指定容量:
new HashMap(expectedSize / 0.75f + 1) - 避免在循环中反复put大量数据却不预设容量
- 并发场景别用HashMap,
ConcurrentHashMap的扩容是分段进行的,不影响读操作
真正难调试的点往往不在put逻辑本身,而在key的hashCode()和equals()实现是否自洽,以及是否无意中把可变对象当key用了——后者会让hash值随对象状态变化,导致get不到、remove失败、甚至内存泄漏。










