HashSet底层基于HashMap实现,key存元素、value为共享的PRESENT对象;其唯一性依赖hashCode()和equals()协同,二者必须同时重写且逻辑一致,否则导致重复;null值被特殊支持,可变对象修改后可能无法查找。

HashSet底层用HashMap实现,key存元素、value固定用PRESSENT
HashSet本身不存储数据,它只是HashMap的一个“外壳”。当你调用add(e)时,实际执行的是map.put(e, PRESENT),而PRESENT是HashMap内部一个共享的静态Object实例。所以“唯一性”本质是HashMap对key的去重逻辑——依赖hashCode()和equals()协同工作。
这意味着:如果两个对象hashCode()不同,它们一定被存为不同元素;但如果hashCode()相同,就一定会触发equals()判断,只有返回false才允许共存。
自定义类必须重写hashCode()和equals(),缺一不可
常见错误是只重写equals(),导致相同逻辑的对象在HashSet中重复出现。因为HashMap先根据hashCode()定位桶(bucket),如果没重写,所有对象默认hashCode()来自Object,彼此不同,根本不会走到equals()比较那步。
- 不重写
hashCode()→ 同值对象散列到不同桶 →add()总成功 → 重复插入 - 不重写
equals()→ 即使hashCode()相同,也认为不相等 → 仍会重复 - 两个方法的计算逻辑必须一致:参与
equals()比较的字段,必须全部参与hashCode()计算
public class User {
private String name;
private int age;
// 必须同时重写
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // 和equals里用的字段完全一致
}
}
hashCode()冲突多会导致性能下降,但不影响正确性
哈希冲突不是bug,而是常态。Java 8之后,当链表长度≥8且桶数组长度≥64时,链表会转为红黑树,把查找从O(n)优化到O(log n)。所以即使大量对象hashCode()相同,只要equals()写得正确,结果依然唯一,只是扩容或查找稍慢。
立即学习“Java免费学习笔记(深入)”;
真正影响性能的是:hashCode()分布极度不均(比如永远返回1),会让所有元素挤在一个桶里,退化成链表遍历。
- 避免在
hashCode()中使用随机数、时间戳、数据库主键(可能为null)等不稳定值 - 字段为
null时,用Objects.hashCode(field)而非field.hashCode(),防止NPE - 如果类是可变的(字段后续会修改),且对象已加入HashSet,修改后可能导致再也查不到——这是设计层面问题,不是哈希机制缺陷
注意null值的特殊处理
HashSet允许存一个null元素,这是由HashMap支持null key决定的。它的处理方式很直接:null的hashCode()定义为0,且HashMap内部有专门分支处理key == null的情况,不调用hashCode()或equals()。
所以如果你的自定义类字段可能为null,不要手动判null然后返回0,应该统一用Objects.hashCode(),它已经做了安全封装。
最容易被忽略的一点:Set的“唯一”是基于对象内容(逻辑相等),不是引用相等。哪怕两个new User("a", 1)是不同实例,只要hashCode()和equals()认定它们相等,就只能存一个。










