重写equals方法必须守住四大底线:一是用getClass()而非instanceof确保类型严格一致;二是用Objects.equals()做null安全比较;三是equals与hashCode必须同步包含相同字段且顺序一致;四是避免循环引用,优先用ID比较而非递归调用。

重写 equals 方法看似简单,但稍不注意就会破坏 Java 的对象契约,导致集合异常、逻辑错乱甚至线上故障。避免常见失误的关键不是“多写几行”,而是守住几个不可妥协的底线。
严格校验类型,别用 instanceof
用 obj instanceof ThisClass 是最常踩的坑——它会让父类和子类比较时违反对称性。比如 User.equals(Admin) 返回 true,但 Admin.equals(User) 却因字段多而返回 false。
正确做法是用 getClass() != obj.getClass() 做类型判定,确保只有同一具体类的实例才可能相等。这符合业务实体的直觉:一个订单不会等于它的子类“促销订单”,除非你明确设计了跨类型语义。
字段比较必须 null 安全
手动调用 field.equals(other.field) 前没判空,运行时就抛 NullPointerException。尤其当字段是数据库映射出来的可空字段(如 String remark),风险极高。
统一使用 Objects.equals(a, b),它内部自动处理两边为 null、一 null 一非 null、都非 null 三种情况,一行代码替代四五行防御逻辑。
所有参与 equals 的字段,必须同步进 hashCode
只改 equals 不改 hashCode,对象放进 HashSet 或作为 HashMap 键时就会“消失”——两个逻辑相等的对象被散列到不同桶里,contains 返回 false,get 找不到值。
用 Objects.hash(field1, field2, field3) 生成哈希码,字段顺序要和 equals 中的比较顺序完全一致。如果 equals 先比 ID 再比 name,hashCode 就不能把 name 放在前面。
警惕循环引用导致的栈溢出
当 A 持有 B、B 又持有 A(如订单与用户、树节点父子互引),直接在 equals 里递归调用对方的 equals,会迅速耗尽栈空间。
优先用唯一标识字段代替递归:比如比 this.user.getId().equals(other.user.getId()),而不是 this.user.equals(other.user)。ID 相同即视为同一实体,既打破循环,又语义清晰。
若真需深度比较(如测试场景验证完整对象图),可用 ThreadLocal 记录当前比较路径,进入前检查是否已存在该对象对,避免重复递归。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











