讨论个问题:该不该重写 equal 和 hashcode

2025 年 12 月 23 日
 WngShhng

和 AI 交流的时候产生的一个问题:

AI 的大致结论是,对于业务类,如果存在明确的唯一标识,比如 ID ,那么应该重写这两个方法,这样在做哈希表之类的时候才能直接使用业务类。

但我认为的是,因为当把一个对象放进哈希表的时候,我会默认它的 hashcode 方法是默认的,也就是每个对象有唯一的哈希值。如果重写了 hashcode ,那么在使用的过程中如果不知道这个类复写了 hashcode ,那么就容易导致代码问题。

所以,我想知道做 java 后端的,你们一般会重写吗?

AI 的一部分原文:

如果抛开 “数量占比”,聚焦于「开发中需要重点设计、保证正确性的核心场景」,重写的频率会远高于 “不重写” 的核心场景,原因如下:

核心类必重写:所有承载业务数据的核心类(如 User 、Order 、Goods 等),几乎 100% 需要重写 —— 这是保证哈希集合正常工作、业务对象唯一性判断、序列化后比对等核心功能的前提,不存在 “可选” 的空间;

重写的 “重复性” 更高:一个项目中,数据承载类的数量虽少,但每个类的开发都必然包含 “重写 equals/hashCode” 这一步(无论是手动生成、Lombok 注解还是 Record 类),属于 “必做操作”;而不重写的类,只是 “无需额外操作”,并非 “主动开发行为”;

工具的普及佐证高频需求:Lombok 的 @EqualsAndHashCode 、Java 16 + 的 Record 类(默认重写)、IDE 自动生成功能,这些工具的广泛使用,本质是因为 “重写” 是开发中的高频需求,才会有大量工具来简化这一操作。
2939 次点击
所在节点    Java
16 条回复
fulln
2025 年 12 月 23 日
能理解 ai 说的重写的必要性, 直接用 Object 的原生 equals 是更直接和方便的做法, 但是我遇到的 99 都是手动拿出来 id 字段做对比的, 这时候重写等于没重写一样。
encounter2017
2025 年 12 月 23 日
> 但我认为的是,因为当把一个对象放进哈希表的时候,我会默认它的 hashcode 方法是默认的,也就是每个对象有唯一的哈希值。如果重写了 hashcode ,那么在使用的过程中如果不知道这个类复写了 hashcode ,那么就容易导致代码问题。

这句话有这么几个误解:
1. “每个对象有唯一的哈希值”,hashcode 只有 2^32 个取值方式
2. “复写了 hashcode ,那么就容易导致代码问题”,只要你不是乱实现,比如 hashCode(anything) = 1, 那不会有啥问题,对于 hashset 的使用场景,冲突了也无所谓(性能会劣化一些),实际会用 equals 兜底


然后重写 equals 必须重写 hashcode, 为啥你可以看下面这个例子就知道了

```java

jshell> import java.util.*;

jshell> class User {
...> int id;
...> User(int id) { this.id = id;}
...>
...> @Override public boolean equals(Object o) {
...> return (o instanceof User u) && this.id == u.id;
...> }
...> // 故意不重写 hashCode() —— 这是错误示范
...> }
| 已创建 类 User


jshell> var set = new HashSet<User>();
set ==> []

jshell> set.add(new User(1));
$5 ==> true


jshell> System.out.println("contains(new User(1)) = " + set.contains(new User(1)));
contains(new User(1)) = false

jshell> System.out.println("equals? " + new User(1).equals(new User(1)));
equals? true
```

然后你如果用过 Record 就知道,调用方不知道是否重写不是风险点,相反它是语言/库的常态用法。

```java
import java.util.*;

record User2(int id) {}

var m = Map.of(new User2(1), "ok");
System.out.println(m.get(new User2(1))); // ok
```

然后什么时候重写 equals: 你需要业务上的相等比较而不是内存地址的比较
比如判断 peronaA == personB, Person(age: Int, name: String)
其实就是比较 person.age 和 person.name 这两个字段

这种情况下重写 equals 必须重写 hashcode ,原因上面说了

简单总结下:
1. 默认 hashCode 不保证唯一(取值空间有限、也可能碰撞)
2. 重写 hashCode 本身不是风险点,风险来自 equals/hashCode 契约被破坏
3. 重写 equals 必须重写 hashCode ,否则 HashSet/HashMap 会出现“看起来相等但查不到”的现象

然后还有一个点:
作为 HashMap/HashSet 的 key 时,参与 equals/hashCode 的字段最好不可变;否则对象放进集合后字段变化,会导致后续 get/contains 失败。

而这些功能和可能踩坑的点 JVM 的 Record ( 2020 年首次 preview ) 都帮你实现了 。作为对比:
Kotiln 1.0 版本在 2016 年作为 data class 的核心关键词支持
而这个功能是 Scala 1.0 早在 2004 年 1.0 发布时就作为 case class 支持了
location123
2025 年 12 月 23 日
安卓仔 和数据相关的 重写好一点 比如 list map 查找等不会出错 该说不说 Kotlin 的数据类真好用
WngShhng
2025 年 12 月 23 日
@encounter2017 “复写容易导致问题”的意思是,如果不知道已经被复写,然后以为是默认实现,就容易导致问题。补充下
encounter2017
2025 年 12 月 23 日
@WngShhng 我还是没太懂,没有场景干说很难理解,你方便具体举一个这种容易出问题的例子,方便理解下吗
WngShhng
2025 年 12 月 23 日
@encounter2017 这就是一个规范而已,很难举例。就是说,不同的对象在内存上分配的地址是不同的,那么很容易因此而认为它们是不同的对象,即便它们在业务上相等,比如相同的用户实体或者有相同的 ID 。

因为很多时候我们拿到一个对象的时候,比如三方框架里的对象,不会去看它有没有复写这两个方法,因此,如果它们被复写了,再按照默认的逻辑去处理,就会导致代码问题。
prosgtsr
2025 年 12 月 23 日
1:保证哈希集合正常工作
我不会把对象实例作为 hashkey ,所以我不会重写,也不喜欢别人重写
2:业务对象唯一性判断
要对比对象时,我也支持在业务里对比需要对比的每个属性,而不是用 equals ,为什么呢?现在有一个类有四个属性,业务对比了四个属性,新人有一天需要再加一个属性,你觉得老业务需要对比第五个属性吗?还有,写这段老代码的人,会喜欢你这么写代码影响他的逻辑吗?影响到老逻辑造成老逻辑出现 bug 的话新人会负责吗?
encounter2017
2025 年 12 月 23 日
@WngShhng 这里说的默认的逻辑指的是啥呢? “不同的对象在内存上分配的地址是不同的” ? 所以 new A equals new A == false 一定成立?没有这种说法吧。。。record 就不是这样的吗?我觉得你的假设站不住脚

@prosgtsr
1. 实际是存在这样的业务场景的,我可以随便给你举两个例子。
a. 序列化/拷贝/深度比较时的“已访问映射”:oldNode -> newNode 的映射表
b. 图遍历(比如 AST 、依赖图、对象引用图)要避免循环:用 Set<Object> 记录“访问过的节点实例”

2. 我觉得你说的理由站不住脚。
bbao
2025 年 12 月 23 日
2009 年左右的日经贴,在 2025 年又见天日。
WngShhng
2025 年 12 月 23 日
@bbao 09 年我还在上初中呢... 那时候的结论是什么?
netabare
2025 年 12 月 23 日
抛开业务、框架、Java 这些问题,equals 和 hashcode 的意义是什么?

我的理解是这是为了构建 equivalence 关系吧。

那么问题是,知道不知道 hashcode 重写,对于 equivalence 的构建和对比,会有影响吗?

HashMap 也好,上游 caller site ,他们做对比的时候会关心 hashcode 是如何使用的,还是说这只是一个契约?

我从这个角度讲会觉得 equals+hashcode 必定是要一起出现的。
guyeu
2025 年 12 月 23 日
重写的 hashCode/equals 方法应该是默认实现的上位替代,所以你的“在使用的过程中如果不知道这个类复写了 hashcode ,那么就容易导致代码问题”是不成立的。

而由于默认实现的天然缺陷(容易把两个逻辑同一的对象当作不同对象)——考虑经典面试题 Integer 池和`new String("")`——在可能作为 Map 的键或 Set 元素的场景中重写 hashCode/equals 是必要的,对于关键的数据对象(如包含 id 字段的 User 实体类),你无法预见它们的使用场景,因此当然应该重写。
xuhengjs
2025 年 12 月 23 日
非必要不用对象最 key 就是了,没事别去重写 hashcode/equals
itechify
2025 年 12 月 23 日
按需重写,需要的时候再重写,但是队友一般 @Data ,算了吧
bbao
2025 年 12 月 24 日
@WngShhng 没必要考虑它,当它不存在,不重写。
franklinyu
2025 年 12 月 28 日
绞尽脑汁都举不出来业务问题,可以等遇到具体业务问题再说。YAGNI 法则了解下

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1180696

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX