重写equals需严守自反性与传递性:自反性保障集合操作可信(如contains、remove),null处理不当会导致npe;传递性破坏将引发hashset去重失败等严重问题,根本解法是禁用继承或用record/final类,并用getclass()严格类型检查。

要让重写的 equals 方法在面试中体现深度,关键不是背条款,而是讲清“为什么必须这样写”以及“不这样写的实际后果”。自反性和传递性看似抽象,其实都直指集合行为的底层逻辑。
自反性:自己必须等于自己,否则集合就“认不出自己”
自反性要求 x.equals(x) 永远返回 true(x 非空)。这不是形式主义——它保障了 ArrayList.contains(x)、HashSet.remove(x) 等操作的基本可信度。一旦失效,对象加进去却查不到,是典型且难定位的 bug。
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 常见翻车点:用字符串处理逻辑但没统一处理
this和other,比如:return this.name.trim().equals(other.name.trim());
若this.name是" Alice ",other.name是"Alice",那this.equals(this)就变成" Alice ".trim().equals(" Alice ".trim())→true,看似没问题;但若字段含 null,this.name为null,调用trim()就直接抛NullPointerException,equals连执行完都做不到,更别说返回true了。 - 安全写法:开头加引用判断 + 使用
Objects.equalsif (this == obj) return true;<br>if (obj == null || getClass() != obj.getClass()) return false;<br>// 后续用 Objects.equals(this.field, other.field)
Objects.equals内部对 null 安全,且天然满足自反性(Objects.equals(a, a)总是true)。
传递性:链条不能断,否则去重和分组会错乱
传递性是五条中最易被忽略、危害最大的一条。它不是“理论正确”,而是直接影响 HashSet 的唯一性、HashMap 的 key 覆盖、甚至 Stream.distinct() 的结果。
- 经典破环场景:父子类分别重写
equals
父类Person只比name;子类Student加了id并扩展比较逻辑。
此时可能出现:
–p1.equals(s1)→true(p1.name == s1.name,Person的equals接受Student)
–s1.equals(s2)→false(s1.id != s2.id)
–p1.equals(s2)→true(还是只比name)
这就导致p1.equals(s1) && s1.equals(s2)为真,但p1.equals(s2)也为真 —— 表面看没破环?注意:真正破环的是s1.equals(p1) && p1.equals(s2)⇒ 应推出s1.equals(s2),但它却是false。 - 根本解法:避免在可继承类中重写
equals
– 优先用record(JDK 14+),编译器自动生成符合全部契约的equals;
– 若必须用普通类,确保它是final,或至少不在父类中提供业务级equals实现;
– 如果真要支持继承,所有子类必须使用完全相同的字段集进行比较(即“公共视图”),且父类用getClass() == obj.getClass()严格限定类型,杜绝跨类型比较。
自反性与传递性背后的共同前提:类型判断必须严谨
这两条看似独立,实则共享一个关键支点:类型检查方式。用 instanceof 容易让父类接受子类(破坏对称性),进而埋下传递性隐患;而 getClass() != obj.getClass() 虽牺牲部分灵活性,却守住契约底线。
- 不推荐:
if (!(obj instanceof Person)) return false;
→ 允许Student向上转型后参与Person的比较,为后续破环打开缺口。 - 推荐:
if (obj == null || getClass() != obj.getClass()) return false;
→ 保证只有同具体类实例才能进入字段比较,从源头掐断不对称与传递链断裂的可能。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










