在Java中为什么要重写equals和hashCode_equals与hashCode设计规范解析

必须重写equals和hashCode以保证逻辑相等对象在HashMap等集合中行为一致:若equals为true则hashCode必相同;二者需遵守自反性、对称性、传递性、一致性及null处理约定,且字段选择须合理。

在Java中重写 equalshashCode,核心原因只有一个:**保证对象逻辑相等时,行为一致且能正确工作于基于哈希的集合(如 HashMapHashSet)中**。不重写,或重写不合规,会导致“明明两个对象内容一样,却查不到”“同一个对象存了两份”等诡异问题。

equals 与 hashCode 必须保持一致性

这是最根本的设计契约:如果两个对象通过 equals 判断为 true,那么它们的 hashCode 值必须相同;反之则不要求(不同对象可以有相同哈希值,即哈希碰撞)。JDK 的集合类(如 HashMap)正是依赖这一约定工作的:先用 hashCode 快速定位桶位置,再用 equals 精确比对键值。

  • 只重写 equals 不重写 hashCode → 逻辑相等的对象可能被散列到不同桶,get()contains() 失败
  • 只重写 hashCode 不重写 equals → 即使哈希值相同,equals 默认比较引用,仍判为不等,集合操作仍出错
  • 两者都重写但逻辑不一致(例如 equals 比较 name+age,hashCode 只用 name 计算)→ 违反契约,行为不可预测

重写 equals 要遵守五项基本约定

equals 方法不是随便写的,它必须满足自反性、对称性、传递性、一致性,以及对 null 的处理。违反任一约定,可能引发 HashSet 重复、TreeSet 异常、甚至并发场景下死循环等严重问题。

  • 自反性:对任意非 null 引用 xx.equals(x) 必须返回 true
  • 对称性:若 x.equals(y)true,则 y.equals(x) 也必须为 true
  • 传递性:若 x.equals(y)y.equals(z)true,则 x.equals(z) 必须为 true
  • 一致性:多次调用结果不变(前提是没有修改影响比较的字段)
  • 对 null 的处理:对任意非 null 引用 xx.equals(null) 必须返回 false

实践中,推荐使用 Objects.equals(a, b) 安全比较字段,避免空指针;用 instanceof + 类型强转做类型检查,而非 getClass() == obj.getClass()(除非明确要求严格类型限制)。

hashCode 的实现要兼顾分布性与确定性

hashCode 不必唯一,但应尽量让逻辑不同的对

象产生不同哈希值(减少碰撞),更重要的是:只要参与比较的字段没变,多次调用必须返回相同值。常见写法是使用 Objects.hash(field1, field2, ...),它自动处理 null 并组合字段哈希。

  • 不要在 hashCode 中使用可变字段(如普通 setter 修改的属性),否则对象加入 HashSet 后再修改字段,就再也找不到了
  • 不要用随机数、当前时间、内存地址等不确定值参与计算
  • 若类是不可变的(如 String、自定义的 Point),用所有关键字段参与哈希计算最稳妥

IDE 和 Lombok 可以帮你生成,但得懂原理

IntelliJ 或 Eclipse 都支持自动生成 equalshashCode 方法,Lombok 的 @EqualsAndHashCode 更是只需一行注解。但生成只是起点——你仍需确认:选了哪些字段?是否包含父类字段?是否忽略某些业务上不该参与比较的字段(如数据库主键 ID、创建时间)?

  • 默认生成通常包含所有非静态字段,可能过度(比如含临时缓存字段)
  • 若继承自某个父类,且父类已重写 equals/hashCode,子类生成时应调用 super.equals()super.hashCode()
  • Lombok 的 callSuper = trueexclude = {"id"} 等参数要按需配置

不复杂但容易忽略。