HashMap用(table.length - 1) & hash计算数组下标,要求容量为2的幂以保证位运算等效取模;hash值经扰动处理,链表长度≥8且数组长度≥64才树化;key必须正确实现hashCode和equals。

HashMap怎么算出数组下标?不是用 % 取模
很多人以为 index = hashCode % table.length 是 HashMap 定位桶的逻辑,其实不是。JDK 8+ 实际用的是位运算:(table.length - 1) & hash。前提是数组长度必须是 2 的幂(如默认 16、32、64…),这样 length - 1 就是形如 0b1111 的掩码,位与操作比取模快得多,也避免负哈希值导致的索引越界问题。
但这也意味着:如果你传入非 2 的幂的初始容量(比如 new HashMap(17)),HashMap 会自动向上找最近的 2 的幂(这里是 32)——所以别硬凑奇数容量,白费力气。
- 自定义 key 类时,
hashCode()要尽量分散,否则高位信息丢失严重,加剧碰撞 - 如果 key 是字符串,Java 已优化过其
hashCode(),一般不用重写;但若 key 是自定义对象,且参与 equals 比较的字段没全用于计算 hashCode,就会出 bug - 注意:
hash()方法在 JDK 中还会对原始hashCode做一次扰动(高 16 位异或低 16 位),进一步降低低位冲突概率
为什么链表长到 8 就转红黑树?不是 7 也不是 9
这个阈值 TREEIFY_THRESHOLD = 8 是经过数学推导和大量压测平衡出来的:泊松分布下,当负载因子为 0.75 时,链表长度达到 8 的概率已低于百万分之一。换句话说,正常均匀哈希下,几乎不会触发树化——它防的是极端碰撞场景,不是日常使用。
但光长度够还不行,还得满足另一个条件:table.length >= MIN_TREEIFY_CAPACITY(即 ≥ 64)。否则即使链表超长,也只扩容,不树化。这是为了防止小数组+短链表就强行树化,反而增加内存和维护开销。
立即学习“Java免费学习笔记(深入)”;
- 红黑树节点比普通 Node 多存父/子/颜色等字段,内存占用约翻倍,小数据量时纯属浪费
- 树化后若后续删除过多,节点数 ≤ 6(
UNTREEIFY_THRESHOLD),会自动退化回链表 - 不要试图通过反射强制树化来“优化”,破坏了 HashMap 的自适应逻辑,反而可能让遍历变慢
put() 时 key 相同却没覆盖?可能是 equals() 或 hashCode() 没写对
HashMap 判断 key 是否存在,分两步:先比哈希值(快速筛),再调用 equals()(精确判)。如果两个逻辑不一致,就会出现“明明是同一个 key,却存了两份”的诡异现象。
典型错误包括:重写了 equals() 却忘了重写 hashCode();或者 hashCode() 依赖了可变字段(如一个 List 成员),而该字段在 put() 后被修改——导致后续 get() 时算出不同哈希,根本找不到原位置。
- key 类必须保证:只要
equals()返回 true,hashCode()就必须返回相同值 - 理想 key 是不可变类(如 String、Integer),或你自己写的 final 字段 + 不可变状态对象
- 调试时可打印
key.hashCode()和map.containsKey(key)结果,快速定位是否哈希/equals 不匹配
扩容时所有元素都要重新哈希?是的,而且可能引发并发问题
当 size > threshold(默认 capacity × 0.75)时,resize() 会被触发:新建两倍长的数组,再把老数组中每个桶的全部节点逐个 rehash 到新位置。这个过程是全量的、阻塞的,对大 Map 来说可能明显卡顿。
更危险的是——HashMap 本身线程不安全。多线程同时 put(),可能因 resize 中的头插法(JDK 7)或节点迁移竞态(JDK 8)导致死循环或数据丢失。这不是“偶尔出错”,而是必然发生,只是时机难复现。
- 如果预估最终 size 是 1000,别用默认构造,直接
new HashMap(1024)(向上取 2 的幂),避免多次扩容 - 永远不要在多线程环境裸用 HashMap;要用
ConcurrentHashMap,或加synchronized(但性能差很多) - 注意:
modCount变量就是为 fail-fast 设计的,遍历时若结构被其他线程修改,立刻抛ConcurrentModificationException
哈希表的“O(1)”只是期望复杂度,实际性能高度依赖哈希质量、容量规划和使用姿势——写对 hashCode 和 equals 是底线,不是加分项。










