hashset底层用hashmap存储,元素作key、present作value;通过hashcode扰动后与数组长度减一按位与定位桶,再用equals判断重复;须同时重写hashcode和equals以保证相等对象哈希值相同且避免哈希冲突;扩容时数组翻倍并重散列以维持o(1)性能;不保证顺序因索引依赖动态数组长度。

HashSet 底层实际是用 HashMap 存储元素的,而 HashMap 本身在 JDK 8+ 中采用 数组 + 链表 + 红黑树 的复合结构。
它怎么利用 hashCode 快速定位
HashSet 不直接管理存储,而是把每个元素作为 key 放进内部的 HashMap,value 固定为一个共享的 Object 常量(PRESENT)。所以“定位元素”本质就是 HashMap 查 key 的过程:
- 调用元素的
hashCode()方法,得到一个整数哈希值 - 对哈希值做扰动运算(JDK 内部优化),再与当前数组长度减一做按位与(
hash & (table.length - 1)),快速算出该元素应落入的数组索引(即桶位置) - 若该桶为空,直接插入;若已有元素,则逐个用
equals()比较——只有 hash 值相同 且 equals 返回 true 才算重复,拒绝添加 - 当同一个桶里链表节点数 ≥ 8 且 数组长度 ≥ 64 时,链表转为红黑树,避免退化成 O(n) 查找
为什么必须重写 hashCode 和 equals
因为去重逻辑严格依赖两者协同:
- 如果两个对象逻辑相等(
equals() == true),但hashCode()不同 → 它们大概率被分到不同桶,contains()就找不到,导致“假重复” - 如果
hashCode()相同但equals()总返回 false → 所有对象都挤在一个桶里,退化为链表遍历,性能崩到 O(n) - 所以自定义类放进 HashSet 时,只要重写其中一个,就必须重写另一个,且保证:相等的对象必须有相同的哈希码
扩容和负载因子的作用
数组初始容量是 16,负载因子默认 0.75。当元素数量达到 16 × 0.75 = 12 时,触发扩容:
- 新建一个容量翻倍(32)的数组
- 所有已有元素重新计算索引,迁移到新数组中
- 这个过程保证了哈希分布更稀疏,降低冲突概率,维持平均 O(1) 性能
它不保证顺序的原因
因为元素存到哪个桶,完全由 hashCode() 和当前数组长度决定;而扩容会改变数组长度,导致同一对象下一次计算出的索引完全不同。迭代时只是按数组从头到尾、再按每个桶内链表或树的顺序访问,和插入顺序毫无关系。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











