Java中equals方法必须严格满足五大契约:自反性(x.equals(x)为true)、对称性(x.equals(y)⇔y.equals(x))、传递性(x=y且y=z⇒x=z)、一致性(字段不变则结果不变)、对null返回false;违反任一契约将导致HashSet、HashMap等集合行为异常。

Java 中使用重写后的 equals 方法,关键不是“怎么调用”,而是“怎么写对”——只有严格遵循五大通用契约,调用结果才可靠,集合行为才稳定。否则看似能比,实际在 HashSet 里查不到、HashMap 里 get() 返回 null、remove() 失败,都是契约被破坏的直接表现。
必须满足的五大契约是什么
这五条不是建议,是 Java 规范强制要求,任何一条不满足都会让对象在标准集合中行为异常:
-
自反性:任意非 null 对象
x,x.equals(x)必须返回true。常见破环点:字段比较前做了trim()或toLowerCase(),但没对this做同样处理 -
对称性:若
x.equals(y)为true,则y.equals(x)也必须为true。最常因instanceof类型检查失守——父类接受子类,子类却不认父类 -
传递性:若
x.equals(y)和y.equals(z)都为true,则x.equals(z)也必须为true。继承场景下极易出问题,比如父类按 name 判等,子类加了 id 后逻辑断裂 -
一致性:只要参与比较的字段没变,多次调用
x.equals(y)结果不能变。可变字段(如状态标志)参与比较会导致对象放入HashSet后再也找不到 -
对 null 的处理:对任意非 null 对象
x,x.equals(null)必须返回false,绝不能抛NullPointerException
重写时的标准操作步骤
以 User 类为例(字段:id: Long、name: String),按契约安全实现:
- 第一句写
if (this == obj) return true;—— 满足自反性,也快速拦截同一引用 - 接着写
if (obj == null || getClass() != obj.getClass()) return false;—— 用getClass()而非instanceof,守住对称性与类型安全 - 强转后,基本类型用
==(如age),引用类型统一用Objects.equals(a, b)—— 自动处理null,避免 NPE,保障一致性 - 只选业务上定义“相等”的字段参与比较,不包含时间戳、状态位等可变字段
为什么必须同步重写 hashCode
equals 和 hashCode 是绑定契约:相等的对象必须有相同的哈希码。如果只改 equals 不改 hashCode:
立即学习“Java免费学习笔记(深入)”;
- 两个
equals返回true的对象,可能被散列到HashMap的不同桶中 -
put()进去了,get()却找不到;add()成功两次,Set出现重复 - 正确做法:用
Objects.hash(field1, field2),参数必须和equals中实际比较的字段完全一致
实用建议与避坑提示
不用从头手写,但得会审、会改、会验:
- IDE 自动生成时,务必核对勾选字段是否真代表“业务相等”——比如日志类对象通常不该把
createTime算进去 - 若类可能被继承,优先考虑用
record(Java 14+)或组合代替继承,天然规避对称性/传递性风险 - 写完立刻验证:用相同字段构造两个对象,测
a.equals(b)、b.equals(a)、a.equals(a)、a.equals(null),再放进HashSet看是否去重 - 避免在
equals中调用可能抛异常或触发远程调用的方法——它可能被集合频繁调用,影响性能与稳定性


















