V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
WngShhng
V2EX  ›  Java

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

  •  
  •   WngShhng · 14 小时 46 分钟前 · 1113 次点击

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

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

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

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

    AI 的一部分原文:

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

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

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

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

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

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

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

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

    而由于默认实现的天然缺陷(容易把两个逻辑同一的对象当作不同对象)——考虑经典面试题 Integer 池和`new String("")`——在可能作为 Map 的键或 Set 元素的场景中重写 hashCode/equals 是必要的,对于关键的数据对象(如包含 id 字段的 User 实体类),你无法预见它们的使用场景,因此当然应该重写。
    xuhengjs
        13
    xuhengjs  
       9 小时 53 分钟前
    非必要不用对象最 key 就是了,没事别去重写 hashcode/equals
    itechify
        14
    itechify  
    PRO
       8 小时 26 分钟前
    按需重写,需要的时候再重写,但是队友一般 @Data ,算了吧
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   962 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 22:36 · PVG 06:36 · LAX 14:36 · JFK 17:36
    ♥ Do have faith in what you're doing.