HashSet不重复是因为底层用HashMap存储元素为key、PRESENT为value,put操作在key已存在时仅覆盖value而不新增节点。

HashSet 底层就是 HashMap,元素全存作 key,value 统一用一个 PRESENT 占位对象。
为什么 HashSet 不重复?靠的是 HashMap.put() 的语义
每次调用 add(e),实际执行的是 map.put(e, PRESENT)。而 HashMap.put() 在 key 已存在时(hashCode() 相同且 equals() 返回 true),只覆盖 value、不新增节点——由于 value 总是 PRESENT,覆盖毫无影响,但 key 不会重复插入,这就天然保证了 Set 的唯一性。
- 没重写
hashCode()和equals()?自定义类往HashSet里 add 多次,大概率全被当成不同元素(因为默认用内存地址算 hash) - 重写了但逻辑不一致?比如
hashCode()用 name 算,equals()却比 name+age——那两个 name 相同 age 不同的对象可能 hash 冲突却判为不等,导致重复添加 -
null是特例:HashMap允许一个nullkey,所以HashSet也只允许一个null
JDK 8+ 的底层结构:数组 + 链表 + 红黑树
和 HashMap 完全一致:HashSet 的桶(bucket)本质是 Node 数组,每个位置要么为 null,要么指向链表头,链表过长(≥8)且数组长度 ≥64 时,该链表会树化成红黑树。
- 初始容量是
16,负载因子默认0.75,所以第 13 个元素插入时触发扩容(resize()) - 扩容后数组长度翻倍(如 16→32),所有已有元素重新计算索引并迁移——这是
add()可能出现“偶发慢”的原因 - 遍历时顺序「看似随机但固定」,是因为它按数组下标 + 链表/树遍历顺序输出,和插入顺序无关
别把 LinkedHashSet 和 HashSet 混为一谈
LinkedHashSet 虽然也继承自 HashSet,但它底层用的是 LinkedHashMap,额外维护了双向链表记录插入顺序;而普通 HashSet 没这层开销,纯哈希定位,性能略高但无序。
立即学习“Java免费学习笔记(深入)”;
- 需要按插入顺序迭代?必须用
LinkedHashSet,HashSet做不到 - 只关心去重和快速查存?选
HashSet,更轻量 - 二者都要求正确实现
hashCode()和equals(),否则去重失效
public class Student {
private String name;
private int age;
// 必须同时重写!缺一不可
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
}
最常被忽略的一点:重写 hashCode() 和 equals() 不是“可选项”,而是使用 HashSet(或任何基于哈希的集合)的前提条件;哪怕只漏掉其中一个,去重行为就不可靠——这不是 bug,是 contract 破坏。










