因为未重写 hashcode 和 equals 或只重写其一,导致 hashset 无法正确识别逻辑相等的对象;java 集合依赖二者协同:先用 hashcode 定位桶,再用 equals 精确比较。

为什么 equals 返回 true,但 HashSet 还是存了两个相同对象?
因为没重写 hashCode 和 equals,或者只重写了其中一个。Java 集合(如 HashSet、HashMap)依赖这两个方法协同工作:先用 hashCode 快速定位桶位置,再用 equals 精确比对。如果只重写 equals,不同对象可能算出不同哈希值,直接分到不同桶里,根本不会触发 equals 判断。
常见错误现象:
- 同一个业务意义的对象(比如两个
new User("alice", 25))被当成不同元素反复添加进HashSet -
HashMap.get(key)返回null,明明刚 put 过这个 key - 用 IDE 自动生成的
equals但忘了勾选hashCode,或手动改了字段却漏同步更新两个方法
哪些字段必须参与 equals 和 hashCode 计算?
只选那些「能唯一标识该对象业务身份」的不可变字段。比如 User 类中,id 是主键,就只用它;如果没 ID,而靠 name + email 联合判断是否为同一人,那就都得参与。别把 createTime 或 status 这类可变字段加进去——对象存进 HashSet 后再改这些字段,哈希值就变了,后续无法被正确查到。
实操建议:
- 优先用不可变字段(
final字段最安全) - 避免使用 null 安全性差的字段,或提前做空判断(
Objects.equals(a, b)自动处理 null) - IDE 生成时,确保
equals和hashCode基于完全相同的字段集合 - 如果用了 Lombok,确认
@EqualsAndHashCode的include或exclude参数写对了,比如@EqualsAndHashCode(onlyExplicitlyIncluded = true, include = {"id"})
hashCode 写成常量或太简单会怎样?
比如所有对象都返回 42,那 HashSet 就退化成链表,插入、查找从 O(1) 变成 O(n),大数据量下性能断崖式下跌。更隐蔽的问题是:某些 JVM 实现或特定集合(如 ConcurrentHashMap)在哈希冲突严重时会触发树化,但树化成本高,且仍不如均匀分布快。
正确做法:
- 用
Objects.hash(field1, field2, ...),它内部做了空安全和混合运算 - 不要手写
31 * a + b这种公式——容易写错,且现代 IDE 和标准库已封装好 - 避免用浮点数字段(
float/double)直接参与计算,它们的NaN行为特殊,Objects.hash已处理
测试你重写的 equals 和 hashCode 是否靠谱?
光看编译通过没用。重点验证三件事:自反性、对称性、一致性。最简单的实操方式是写个单元测试,往 HashSet 里塞两个逻辑相等的对象,看 size 是不是 1。
示例片段:
Set<User> set = new HashSet<>();
set.add(new User("alice", 25));
set.add(new User("alice", 25)); // 如果重写正确,这行不生效
assert set.size() == 1;容易被忽略的点:
- 子类继承父类后,如果子类新增了影响相等性的字段,必须重写两个方法并调用
super.equals()/super.hashCode() - 用作
Map的 key 时,对象在放入后绝不能修改参与hashCode计算的字段,否则 key “消失”——找不到也删不掉 - 序列化/反序列化后的对象,
hashCode应该和原对象一致,否则跨进程场景下集合行为异常









