重写 equals 必须同时重写 hashcode,且二者须保持一致:若 equals 返回 true,则 hashcode 必须相同;hashcode 应仅基于不可变字段计算,并涵盖 equals 中所有比较字段,否则哈希集合将失效。

重写 equals 必须满足自反、对称、传递、一致和非空五条契约
Java 集合(如 HashSet、HashMap)依赖 equals 判断逻辑相等,但如果你只重写 equals 而不重写 hashCode,对象在哈希集合中大概率无法被正确查找或去重——因为哈希桶定位靠 hashCode,内容比对才用 equals。
常见错误现象:new Person("Alice", 25) 和 new Person("Alice", 25) 手动 .equals() 返回 true,但放进 HashSet 后仍出现两个实例。
- 必须用所有参与逻辑比较的字段(即
equals中用到的字段)来计算hashCode - 推荐用
Objects.equals(a, b)和Objects.hash(...),它们自动处理null,避免 NPE - 不要在
equals中强转失败就静默返回false,要先用instanceof判类型(尤其继承场景)
hashCode 实现不一致会导致 HashMap 查不到键或 HashSet 重复添加
只要两个对象 equals 返回 true,它们的 hashCode 就必须相同;反之不成立。违反这点,哈希结构会直接失效。
典型坑:hashCode 里漏掉某个 equals 中比较的字段,比如只用了 name 却忘了 age;或者用了可变字段(如 ArrayList 引用),后续修改后哈希值变化,对象在 HashSet 中“消失”了。
- 确保
hashCode计算仅依赖不可变字段(final修饰更安全) - 避免用
Math.random()、System.nanoTime()或当前时间戳——这些会让哈希值每次都不一样 - 如果类有继承关系,父类已重写
hashCode,子类必须把父类hashCode结果纳入计算(如super.hashCode() * 31 + ...)
IDE 自动生成的 equals/hashCode 够用,但要注意字段选择
IntelliJ / Eclipse 生成的代码基本符合规范,但默认可能包含你不希望参与比较的字段(如数据库主键 id、缓存字段、临时状态标记)。
例如:一个 JPA 实体类,id 是自增主键,新建对象时为 null,若把它加入 equals,两个新对象永远不等——这通常不符合业务语义(你可能希望按业务字段如 email 和 phone 去重)。
- 生成时手动取消勾选无关字段,尤其是可变、临时、持久化专用字段
- 如果用 Lombok,
@EqualsAndHashCode(exclude = "id")比全量生成更可控 - 测试时务必覆盖
null字段、空字符串、数值边界值等 case,别只测“正常数据”
使用 record 可彻底规避手写错误(Java 14+)
如果你的类是纯数据载体(无行为、字段全 final、构造即赋值),record 是最省心的选择:它自动实现 equals、hashCode、toString,且语义清晰、不可变、线程安全。
例如:record User(String name, int age) {},两个 User("Tom", 30) 实例放进 Set 就天然唯一。
- 不能用于需要继承、懒加载、复杂构造逻辑或运行时修改字段的场景
- 字段类型本身也得合理实现
hashCode,比如自定义类字段没重写hashCode,record 的hashCode也会出错 - 注意 record 的
canonical constructor无法绕过字段校验,若需容错(如允许null名字),就得退回到普通类
真正容易被忽略的是:集合去重发生在对象插入时,不是读取时。一旦对象进了 HashSet,之后再改它的字段(哪怕只是 setter 修改了一个参与 hashCode 的属性),这个对象就再也找不到了——既不在原桶里,也不在新桶里。










