应使用 getclass() 代替 instanceof 判断类型,强制同类型比较以保障 equals 的对称性与传递性;判等字段须全量一致且不可变;优先采用 record 或 final 类避免继承引发的语义分裂。

在继承体系中,子类重写 equals 时最容易破坏传递性——不是因为写错了逻辑,而是因为“扩展判等字段”与“类型兼容性”之间产生了隐性冲突。核心原则只有一条:判等依据必须统一、稳定、不可变,且不能因子类存在而分裂语义。
用 getClass() 锁死比较范围,避免跨类型链式比较
传递性失效常始于对称性漏洞:父类用 instanceof Parent 接受子类对象,子类却用 instanceof Child 拒绝父类对象,导致 p.equals(s) 为 true 而 s.equals(p) 为 false。一旦对称崩塌,传递性就失去基础。
- 一律用
this.getClass() == obj.getClass()替代instanceof,强制只同类型实例间比较 - 这样能天然切断“父类实例 ↔ 子类实例 ↔ 另一子类实例”的错误传递链
- 若业务真需跨类型相等(如 DTO 与 Entity),应由顶层抽象类定义统一判等契约,且禁止子类覆盖
equals
判等字段必须全量一致,不增不减不选择性启用
传递性崩溃的典型场景是:父类按 name 判等,子类加了 id 后只在自身实例间用 id 比较,结果出现 p1(name=A).equals(s1(name=A,id=1)) → true,s1.equals(s2(name=A,id=2)) → false,但 p1.equals(s2) → true —— 形成矛盾三角。
- 要么所有相关类都只用同一组字段(如全部依赖数据库主键
id) - 要么全部不用可变或业务字段(如避免用
status、lastModified) - 子类若新增标识字段,必须同步纳入父类的判等逻辑(即父类也需感知该字段),否则就放弃继承,改用组合或 record
优先用 record 或 final 类替代可继承的普通类
Java 的继承机制与 equals 的数学等价性本质冲突。record 自动基于全部字段生成 equals,且不可被继承;final 类则从源头杜绝子类篡改判等逻辑。
- value 类型对象(如订单明细、坐标点、配置项)直接声明为
record,无需手写equals - 若需行为封装,把状态封装进 final 类内部,对外暴露不可变视图
- 实在要用继承,就把
equals放在最顶层 final 类中,子类仅扩展方法,不重写判等逻辑
三元组手动验证:别只测两两相等
单元测试不能只写 a.equals(b) 和 b.equals(c),必须构造至少一组含不同类型的三对象组合,覆盖潜在断裂点。
- 例如:准备
p = new Person("Alice")、s1 = new Student("Alice", 101)、s2 = new Student("Alice", 102) - 检查
p.equals(s1)、s1.equals(s2)、p.equals(s2)的真假组合是否自洽 - 再交换参数顺序跑一遍,确认
s1.equals(p)结果与p.equals(s1)完全相同
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











