必须同时重写equals和hashcode,否则hashset/hashmap中会出现重复对象;二者须用相同字段计算,且字段应为不可变的业务主键。

为什么 equals 和 hashCode 必须一起重写
只重写 equals 而不重写 hashCode,会导致对象在 HashSet 或 HashMap 中重复存入——这是最常踩的坑。Java 规范明确要求:如果两个对象 equals 返回 true,它们的 hashCode 必须相等;反之不成立。
常见错误现象:Set<person> set = new HashSet(); set.add(new Person("Alice", 25)); set.add(new Person("Alice", 25));</person> 结果 set.size() 是 2,而不是 1。
- 必须用相同字段参与
equals和hashCode计算(比如都用name和age) - 推荐用 IDE 自动生成(IntelliJ 快捷键
Alt+Insert→ “equals() and hashCode()”),避免手写漏判null或逻辑不一致 - 字段值一旦参与计算,就应视为“业务主键”,后续不可随意修改(否则已加入集合的对象可能再也找不到了)
使用 TreeSet 去重时要实现 Comparable 接口
如果不想重写 equals/hashCode,或需要按特定顺序去重(比如按年龄升序),可以用 TreeSet,但它依赖 compareTo 方法判断唯一性,和哈希集合逻辑完全不同。
使用场景:去重同时要求有序、且对象天然有可比性(如时间、编号、名称)。
立即学习“Java免费学习笔记(深入)”;
-
TreeSet判定“重复”的标准是compareTo == 0,不是equals - 若实现
Comparable,必须保证compareTo与equals逻辑一致,否则Collections.binarySearch等操作行为异常 - 示例:
public int compareTo(Person o) { return Objects.compare(this.name, o.name, String::compareTo); }—— 此时仅用name去重,age不影响唯一性
Stream + distinct() 的实际限制
stream.distinct() 底层仍调用对象的 equals 方法,所以它和 HashSet 去重效果一致,但容易被误以为“更高级”或“自动处理”。它没有绕过重写 equals/hashCode 的必要。
性能影响:每次 distinct() 都会新建一个 LinkedHashSet 缓存已见元素,内存开销略大;对大数据量不如预建 Set 后 addAll 直观。
- 不能传入自定义比较器(Java 17 之前),想按部分字段去重就得先
map成新对象或用collectingAndThen配合toMap - 错误用法:
list.stream().distinct().collect(Collectors.toList())在没重写equals时完全无效 - 替代方案(按
name去重):list.stream().collect(Collectors.toMap(p -> p.getName(), p -> p, (a, b) -> a)).values()
字段选错导致去重失效的典型情况
最容易被忽略的是:把可变字段(如状态、更新时间)或非业务主键字段(如数据库自增 id)放进 equals/hashCode,结果对象一更新就“从集合里消失”了。
真实场景举例:用户登录后修改昵称,但 Person 类把 nickname 写进了 hashCode,导致缓存中的该用户无法被 set.contains() 找到。
- 只选用不可变(
final)、能代表业务唯一性的字段,比如userId、email、组合键tenantId + code - 如果类本身是 DTO 或从 DB 查询而来,优先用数据库主键或业务唯一索引字段,别用全字段
- 用 Lombok?注意
@Data会为所有字段生成equals/hashCode,要用@EqualsAndHashCode(onlyExplicitlyIncluded = true)显式控制









