HashSet 存储自定义对象去重依赖正确重写 hashCode() 和 equals() 方法,二者必须逻辑一致且不可缺一;底层基于 HashMap 的 key 唯一性机制,通过先 hash 后 equals 判断重复;对象加入后若修改参与计算的字段会导致查找失败,建议设计为不可变;null 值被特殊处理,仅允许一个。

Java 中 HashSet 存储自定义对象不重复,关键不是靠 HashSet 自己“检查重复”,而是靠你写的类正确实现 hashCode() 和 equals() 方法,并让两者逻辑一致。HashSet 本身只是严格执行这两者的协同规则。
底层靠 HashMap 的 key 唯一性
HashSet 内部其实是一个 HashMapadd(obj),本质是执行 map.put(obj, PRESENT)。而 HashMap 的 key 天然不允许重复——它判断 key 是否重复,就只看两步:
- 先算
obj.hashCode(),决定存到哪个桶(数组索引) - 如果桶里已有元素,再对每个元素调用
obj.equals(已存在对象)
只有 hashCode 相同 且 equals 返回 true,才认为是同一个 key,新值会覆盖旧值(即 add 失败)。所以,你的类不重写这两个方法,就等于把去重权交给了 Object 默认实现——按内存地址判等,自然无法识别业务上“相同”的对象。
必须同时重写 hashCode 和 equals
只重写 equals() 不行,只重写 hashCode() 也不行,二者缺一不可,且要保持契约:
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
立即学习“Java免费学习笔记(深入)”;
- 如果
a.equals(b) == true,那么a.hashCode() == b.hashCode()必须为 true - hashCode 应只基于 equals 中用到的、**不会改变**的字段计算(比如 id、name、age,而不是临时状态)
- 推荐用
Objects.hash(field1, field2)生成 hashCode,简洁且不易出错 - equals 要做 null 检查、类型检查、字段逐一对比,避免 NPE 和 ClassCastException
可变字段改了会导致“找不到了”
一旦对象加入 HashSet,又修改了参与 hashCode/equals 计算的字段(比如把 Person 的 name 改了),问题就来了:
- 原来它被放在 hash 值 A 对应的桶里
- 现在重新计算 hash,变成值 B,查找时会去桶 B 找,但桶 A 里还留着它
- 结果
contains()返回 false,remove()失效——它没丢,只是“隐身”了
所以,放进 HashSet 的对象,最好设计成不可变(immutable),或者至少确保关键字段在加入后不再变更。
null 值是特例,能存且唯一
HashSet 明确支持一个 null 元素。它的处理是硬编码的:所有 null 都被映射到哈希表索引 0 的位置,且只允许存一个。不需要你重写方法来支持 null,这是 JDK 已经做好的保障。

















