讨论个问题:该不该重写 equal 和 hashcode - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
WngShhng
V2EX    Java

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

  •  
  •   WngShhng 1 天前 1518 次点击

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

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

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

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

    AI 一部分原文:

    如果抛开 “数量占比”,聚焦于「开发中需要重点设计、保证正确性的核心场景」,重写的频率会远高于 “不重写” 的核心场景,原因如下: 核心类必重写:所有承载业务数据的核心类(如 User 、Order 、Goods 等),几乎 100% 需要重写 这是保证哈希集合正常工作、业务对象唯一性判断、序列化后比对等核心功能的前提,不存在 “可选” 的空间; 重写的 “重复性” 更高:一个项目中,数据承载类的数量虽少,但每个类的开发都必然包含 “重写 equals/hashCode” 这一步(无论是手动生成、Lombok 注解还是 Record 类),属于 “必做操作”;而不重写的类,只是 “无需额外操作”,并非 “主动开发行为”; 工具的普及佐证高频需求:Lombok 的 @EqualsAndHashCode 、Java 16 + 的 Record 类(默认重写)、IDE 自动生成功能,这些工具的广泛使用,本质是因为 “重写” 是开发中的高频需求,才会有大量工具来简化这一操作。 
    第 1 条附言    1 天前
    或者说就是在理解上容易让人以为内存地址不同,那么就应该是不同的对象
    15 条回复    2025-12-24 10:57:53 +08:00
    fulln
        1
    fulln  
       1 天前
    能理解 ai 说的重写的必要性, 直接用 Object 的原生 equals 是更直接和方便的做法, 但是我遇到的 99 都是手动拿出来 id 字段做对比的, 这时候重写等于没重写一样。
    encounter2017
        2
    encounter2017  
       1 天前
    > 但我认为的是,因为当把一个对象放进哈希表的时候,我会默认它的 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
        3
    location123  
       1 天前
    安卓仔 和数据相关的 重写好一点 比如 list map 查找等不会出错 该说不说 Kotlin 的数据类真好用
    WngShhng
        4
    WngShhng  
    OP
       1 天前
    @encounter2017 “复写容易导致问题”的意思是,如果不知道已经被复写,然后以为是默认实现,就容易导致问题。补充下
    encounter2017
        5
    encounter2017  
       1 天前
    @WngShhng 我还是没太懂,没有场景干说很难理解,你方便具体举一个这种容易出问题的例子,方便理解下吗
    WngShhng
        6
    WngShhng  
    OP
       1 天前
    @encounter2017 这就是一个规范而已,很难举例。就是说,不同的对象在内存上分配的地址是不同的,那么很容易因此而认为它们是不同的对象,即便它们在业务上相等,比如相同的用户实体或者有相同的 ID 。

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

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

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

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

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

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

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

    而由于默认实现的天然缺陷(容易把两个逻辑同一的对象当作不同对象)考虑经典面试题 Integer 池和`new String("")`在可能作为 Map 的键或 Set 元素的场景中重写 hashCode/equals 是必要的,对于关键的数据对象(如包含 id 字段的 User 实体类),你无法预见它们的使用场景,因此当然应该重写。
    xuhengjs
        13
    xuhengjs  
       23 小时 36 分钟前
    非必要不用对象最 key 就是了,没事别去重写 hashcode/equals
    itechify
        14
    itechify  
    PRO
       22 小时 10 分钟前
    按需重写,需要的时候再重写,但是队友一般 @Data ,算了吧
    bbao
        15
    bbao  
       9 小时 22 分钟前
    @WngShhng 没必要考虑它,当它不存在,不重写。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2954 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 12:20 PVG 20:20 LAX 04:20 JFK 07:20
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86