Set.add() 拒绝重复元素依赖 HashMap 的 key 去重机制:先用 hashCode() 定位桶,再用 equals() 精确判断;必须同时重写 hashCode() 和 equals() 且保持契约一致,否则导致重复添加或查找失败。

Set.add() 为什么能拒绝重复元素
靠的是 HashSet 底层用 HashMap 存储时对 key 的去重逻辑——不是靠遍历比对,而是靠 hashCode() 定位桶位置,再用 equals() 精确判断是否已存在。
常见错误现象:new HashSet<Person>().add(new Person("Alice", 25)); 加了两次一模一样的对象,结果 size 变成 2。这是因为没重写 hashCode() 和 equals(),两个新对象的 hashCode() 不同(默认是内存地址),压根不会走到 equals() 比较那步。
- 必须同时重写
hashCode()和equals(),只改一个等于白改 -
equals()返回true的两个对象,hashCode()必须返回相同值(这是契约) - 反之不成立:
hashCode()相同,equals()不一定为true(哈希冲突正常)
重写 equals() 时漏掉 null 或类型检查会怎样
典型翻车代码:public boolean equals(Object o) { return this.name.equals(o.name); } —— 一旦传入 null 或非 Person 类型,直接抛 NullPointerException 或 ClassCastException,导致 Set.contains()、remove() 全挂。
正确写法必须包含三段式检查:
立即学习“Java免费学习笔记(深入)”;
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name) && age == person.age;
}
- 第一行处理自反性(自己等于自己)
- 第二行拦截
null和类型不匹配,避免运行时异常 - 第三行才做字段比较,且用
Objects.equals()安全处理null字符串
hashCode() 用错字段或计算方式导致 Set 失效
比如 Person 类里只拿 name 算 hashCode(),但 equals() 却比较 name + age,就会违反契约:两个 age 不同但 name 相同的对象,hashCode() 相同,equals() 却返回 false —— 这会让它们被塞进同一个桶,但 Set 误判为“可能相等”,实际又不等,造成逻辑混乱甚至内存泄漏。
-
hashCode()用到的字段,必须和equals()判定逻辑完全一致 - 推荐用
Objects.hash(name, age)生成,别手写乘加(易错且难维护) - 如果字段可变(如后续会修改
name),放进HashSet后再改,会导致对象“丢失”——它原来在桶 A,改完 hash 变成桶 B,但没人通知 Set 去挪位置
TreeSet 场景下 equals/hashCode 根本不生效
如果你用的是 TreeSet,那上面所有关于 hashCode() 和 equals() 的讨论都跑偏了——TreeSet 不依赖哈希,它靠 Comparable 或 Comparator 排序,唯一性由 compareTo() 或 compare() 的返回值决定:返回 0 就算重复。
常见错误:类实现了 Comparable,但 compareTo() 逻辑和 equals() 不一致。例如 compareTo() 只比 name,而 equals() 还要看 id。这时 TreeSet 会把 name 相同但 id 不同的两个对象当成重复,直接吞掉第二个。
-
TreeSet的“唯一性”定义和HashSet完全不同,不能混用预期 - 如果必须用
TreeSet且需要业务上严格唯一,确保compareTo()和equals()语义一致,或者干脆用TreeSet+ 自定义Comparator控制逻辑 - 没有实现
Comparable且没传Comparator,往TreeSet里 add 就抛ClassCastException
HashSet 后还去改影响 hashCode() 的字段,这比写错方法更隐蔽,也更难排查。










