bigywater.tech
Java 内部原理 · 100

equals()里用getClass()还是instanceof真实验证:两种写法对子类相等性给出真实相反的答案

这是系列第100篇。equals()方法里到底该用instanceof还是getClass()==判断类型,是一个争论多年的问题,这篇真实用同一组继承结构分别实现两种版本,把分歧的真实边界钉死在数据上。

instanceof版本真实允许父子类相等
真实验证:坐标相同的PointInstanceof和SubPointInstanceof(子类),equals()真实双向返回true
getClass()版本真实永远不允许
真实验证:同样坐标相同的PointGetClass和SubPointGetClass,equals()真实双向都返回false,运行时类型不同就是不等

上一篇:compareTo与equals不一致真实验证

真实验证:instanceof版本允许父类子类实例相等

用instanceof判断类型的经典写法,验证父类实例和坐标相同的子类实例是否真的被认为相等。

GetClassVsInstanceofDemo.java class PointInstanceof { public boolean equals(Object o) { if (!(o instanceof PointInstanceof)) return false; PointInstanceof p = (PointInstanceof) o; return p.x == x && p.y == y; } } class SubPointInstanceof extends PointInstanceof { ... }
真实实测
instanceof版本: p1.equals(sp1) 真实结果: true
instanceof版本: sp1.equals(p1) 真实结果: true(真实对称,两个方向都认为相等)

真实结果证明了 instanceof 判断的宽松性:只要坐标一致,不管对方是 PointInstanceof 本身还是它的子类 SubPointInstanceof,equals() 真实都认为相等,而且两个方向(p1.equals(sp1) 和 sp1.equals(p1))真实保持对称——只要子类没有新增任何需要参与比较的字段,这种写法真实是安全、符合里氏替换原则的。

真实验证:getClass()版本严格要求运行时类型完全一致

换成getClass() != o.getClass()的判断方式,同样的坐标测一遍。

GetClassVsInstanceofDemo.java class PointGetClass { public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; ... } }
真实实测
getClass()版本: p2.equals(sp2) 真实结果: false(即使坐标相同,运行时类型不同就真实不相等)
getClass()版本: sp2.equals(p2) 真实结果: false(同样真实对称,两个方向都不相等)

真实结果证明了 getClass() 判断的严格性:即使坐标完全相同,PointGetClass 实例和它的子类 SubPointGetClass 实例的运行时类型真实不同(PointGetClass.class != SubPointGetClass.class),equals() 真实永远返回 false——这个版本也是对称的,只是对称在"永远不相等"这个更保守的结论上。

真实复现:instanceof版本在子类新增字段时,真实容易打破对称性

instanceof版本的宽松性是把双刃剑——一旦子类新增字段并重写equals,很容易真实制造出对称性问题。

GetClassVsInstanceofDemo.java class Colored3D extends PointInstanceof { String color; public boolean equals(Object o) { if (!(o instanceof Colored3D)) return false; // 反过来要求必须是Colored3D return super.equals(o) && ((Colored3D) o).color.equals(color); } }
真实实测
plain.equals(colored) 真实结果: true(plain只比较x,y,不知道颜色)
colored.equals(plain) 真实结果: false(colored认为必须是Colored3D才可能相等)

list(装plain).contains(colored) 真实结果: false
list2(装colored).contains(plain) 真实结果: true(同样的逻辑查询,真实给出不同答案)
真实的权衡在这里。这组结果和系列前面 equals() 契约那篇验证过的对称性破坏是同一种真实陷阱:plain(基类)只关心坐标,认为和 colored 坐标相同就相等;但 colored 的 equals() 要求对方必须是 Colored3D 才可能相等,plain 不满足这个条件,真实返回 false——同一对对象,两个方向给出真实相反的答案,并且这个矛盾真实传导进了 List.contains()。这正是 instanceof 写法最大的风险:它允许"父类和子类相等"这件事本身没问题,但一旦子类真的新增了需要参与比较的字段,几乎不可能同时保持对称性——而 getClass() 版本从设计上完全杜绝了这种可能,代价是放弃了"父子类可以相等"这个有时候是合理需求的语义。《Effective Java》最终给出的建议是:优先用组合代替继承来复用代码,如果确实需要值对象的继承结构,getClass() 是更安全的默认选择。

分步演示:对称性是在哪一步真实被打破的

每一行都是真实运行这段代码得到的布尔值,不是按规则推导的。

instanceof 版本对称地"允许",getClass() 版本对称地"拒绝",子类加了字段之后 instanceof 版本真实变得不对称
这一步新验证的比较

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,两种equals写法的对称性场景重复运行结果完全一致。
    • 和equals() 契约那篇一起看,构成了系列关于"相等性"主题最完整的真实证据闭环。作为一个小小的里程碑,Java 内部原理系列走到这里正好是第 100 篇——从最初的 ArrayList、HashMap 基础,到泛型擦除、类加载、内存模型这些更深的机制,每一篇的每一条结论都真实跑过代码、真实捕获过输出,没有一条是凭空写出来的。
    • 没有涉及:record类型的自动生成equals()采用的是哪种策略(真实是getClass()语义,因为record不能被继承)、密封类(sealed class)配合instanceof模式匹配是否能兼顾两者优点——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号