重写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),风险极高。
立即学习“Java免费学习笔记(深入)”;
统一使用 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<set>></set> 记录当前比较路径,进入前检查是否已存在该对象对,避免重复递归。


















