重写equals方法必须严格遵守自反性、对称性、传递性、一致性与非空性五大特性,否则HashSet、HashMap等集合操作将不可靠,如查找不到对象、重复添加或remove失败。

重写 equals 方法时严格遵守自反性、对称性、传递性,不是为了应付规范,而是直接决定 HashSet、HashMap、ArrayList.contains() 等集合操作是否可靠。一旦违反,就可能出现“明明对象一样却查不到”“同一个对象被加进 Set 两次”“remove 失败”等现象。
自反性没满足 → 集合判断直接失灵
自反性要求:对任意非 null 对象 x,x.equals(x) 必须返回 true。
如果返回 false,集合底层(比如 ArrayList.indexOf())在查找自身时就会失败。
- 典型错误:字段比较时做了 trim() 或 toLowerCase(),但没对当前对象做同样处理
- 例如:
new Person("Alice ")的 name 带空格,equals中用obj.getName().trim()比较,但没对this.name.trim(),导致p.equals(p)返回false - 修复方式:所有字符串/对象比较前统一预处理,或改用
Objects.equals(this.name, other.name)(它自动处理 null 和相等情况)
对称性被破坏 → HashMap key 查不到、Set 重复添加
对称性要求:x.equals(y) == true 当且仅当 y.equals(x) == true。
违反后最常见后果是:用子类对象和父类对象互相作为 HashMap 的 key,put 进去了却 get 不出来;或者 Set 认为两个对象不等,重复添加。
- 高频诱因:用
instanceof判断类型,子类equals允许与父类比较,但父类equals不接受子类(因为instanceof Parent对子类实例返回true,但instanceof Child对父类实例返回false) - 安全做法:用
getClass() == obj.getClass()替代instanceof,确保类型完全一致(除非你明确需要跨类比较,那就得父子类双方协同设计) - 更推荐:避免在父类中重写
equals后又让子类也重写——改用组合或记录类(record),天然规避该问题
传递性失效 → 集合逻辑崩溃,结果不可预测
传递性要求:若 x.equals(y) 且 y.equals(z) 为 true,则 x.equals(z) 也必须为 true。
这个最容易被忽略,但危害最大:可能让 TreeSet 排序错乱、HashMap 在扩容时把本该同桶的对象分到不同桶,甚至引发 ConcurrentModificationException 类似表现。
- 经典陷阱:父类
Person按 name 比较,子类Student加了 id 字段并扩展equals,结果出现p1(name=A).equals(s1(name=A,id=1)) → true,s1.equals(s2(name=A,id=2)) → false,但p1.equals(s2) → true,造成s1.equals(s2)与传递链矛盾 - 根本解法:不混合继承式扩展判等逻辑;如必须,统一用
getClass()限定类型,或改用“字段白名单”方式(比如只比业务主键,而非所有字段) - 实用建议:优先使用
Objects.equals(a, b)比较每个字段,它不抛 NPE,且语义清晰;整个方法体保持扁平、无分支逻辑,降低出错概率
别忘了 hashCode —— equals 的绑定搭档
只要重写了 equals,就必须重写 hashCode,否则 HashMap、HashSet 等基于哈希的集合会彻底失效:两个 equals 返回 true 的对象,hashCode 却不同,就会被散列到不同桶里,永远无法命中。
- 正确姿势:用
Objects.hash(field1, field2, ...)生成 hash,它与Objects.equals的字段顺序和逻辑严格对应 - 验证技巧:写完后手动测试
new A().equals(new A())、a.equals(b)及其反向、以及多个实例间的传递链,再检查a.hashCode() == b.hashCode() - 工具辅助:IDE(如 IntelliJ)可自动生成符合规范的
equals和hashCode;Lombok 的@EqualsAndHashCode也能省力,但需确认字段列表合理

















