Java中equals怎么用?——全面解析 Java 中 equals 常用用法及深度实践
从基础原理到高级陷阱,手把手教你正确使用 equals 方法,避免常见坑点,提升代码健壮性与可维护性
equals 方法的核心定位:逻辑相等 vs 引用相等
在 Java 的世界里,equals 方法简直就像那个一辈子让你脸红又尴尬的“老好人”——它存在的唯一理由,就是让你认定:“咦?这对象仿佛能跟我扯上关系啊”。
你当作它只是个一般/平平的函数,调用它就能判断两个东西“是不是”一样?大错特错。在 Java 的面向对象哲学里,equals 跟 == 彻底是两条道。
==:内存地址指针对吗?
判断两个引用是否指向同一个对象实例(即内存地址相同),属于物理层面
equals:内容逻辑上对吗?
判断两个对象在业务语义上是否“相等”,属于逻辑层面
这就好比店里卖可乐。你拿一瓶可乐和另一瓶拿过来问老板:“这俩是一样吗?”老板不一定只看瓶子里是不是有可乐,他可能更关心瓶子里装的口味、防腐剂、就连生产日期是不是对得上——这就是 equals 的活法。
Object 类中定义的 equals 方法默认行为与 == 相同(即引用比较),这是 Java 语言设计的起点,但绝非终点。真正体现 equals 价值的,是你在自定义类中对其的重写。
equals 方法的默认实现
在 java.lang.Object 中,equals 方法的源码如下:
public boolean equals(Object obj) {
return (this == obj);
}
可见,默认实现仅比较引用地址。这意味着:如果你不重写 equals,那么 equals 与 == 的行为完全一致。
为什么需要重写 equals?
想象以下场景:
- 你有一个
User类,包含id、name、email字段。你想判断两个用户是否是“同一个人”——逻辑上应基于id或email,而非内存地址。 - 你有一个
Point类,坐标为(3, 4)。你想判断两个点是否重合——应比较数值,而非引用。 - 你从数据库查出两个
Book对象,内容完全一致,但它们是两个独立对象——你希望它们“相等”。
若不重写 equals,上述所有场景都将返回 false,导致业务逻辑错误。
equals 方法的通用契约(Contract)
根据 Java 规范,重写的 equals 必须满足以下五条契约:
- 自反性:对任意非 null 引用
x,x.equals(x)应返回true。 - 对称性:对任意非 null 引用
x和y,若x.equals(y)为true,则y.equals(x)也应为true。 - 传递性:对任意非 null 引用
x、y、z,若x.equals(y)为true且y.equals(z)为true,则x.equals(z)应为true。 - 一致性:对任意非 null 引用
x和y,多次调用x.equals(y)应始终返回相同结果(前提是对象未被修改)。 - 非空性:对任意非 null 引用
x,x.equals(null)应返回false。
违反任一契约,都可能导致集合类(如 HashSet、HashMap)行为异常。
equals vs ==:一次彻底的对比分析
“你当作 a.equals(b) 等于 a == b,结局发现两者天壤之别。”
String s1 = "hello";
String s2 = "he" + "llo";
String s3 = new String("hello");
System.out.println(s1 == s2); // true(编译期常量折叠)
System.out.println(s1 == s3); // false(不同对象)
System.out.println(s1.equals(s3)); // true(内容相同)
很多开发者一到 equals 就懵了,下意识想用 == 偷懒,结局代码一跑就崩。
- 比较基本类型(int、char、boolean 等)
- 明确知道两个引用指向同一对象(如循环中判断
list.contains(item)) - 判断对象是否为
null(如if (obj == null))
- 比较对象内容是否“逻辑相等”(如用户ID、订单号、日期)
- 使用集合类时(如
List.contains()、Map.containsKey()) - 业务逻辑中需要“语义相等”判断(如“同一个商品”“同一笔交易”)
equals 的典型误用场景
若你在拼接字符串后使用 == 比较,结果可能因 JVM 优化而不同。例如:
String a = "ab";
String b = "a" + "b";
String c = new String("ab");
System.out.println(a == b); // true(常量池复用)
System.out.println(a == c); // false(堆中新建对象)
而 a.equals(c) 始终为 true。
equals 的性能影响
在循环中频繁调用 equals(如嵌套循环比较两个列表)可能造成性能瓶颈。建议:
- 优先使用
hashCode预过滤(如先比哈希值,再比内容) - 对频繁比较的对象实现缓存机制(如缓存计算过的哈希值)
- 避免在热路径中使用复杂逻辑的
equals(如递归比较嵌套对象)
null 处理:最容易被忽视的坑
“要是两个对象都不想比,要么根本没法比(比如一个是 null 一个是字符串),equals 会对处理这些边界情况。”
equals 的空指针风险String s = null;
System.out.println(s.equals("test")); // NullPointerException!
这比 == 更危险:因为 null == "test" 直接返回 false,而不会抛异常。
安全调用 equals 的三种方式
✅ 推荐:将常量作为调用者(避免空指针)
// ✅ 安全:即使 s 为 null,也不会抛异常
if ("test".equals(s)) {
System.out.println("匹配");
}
✅ 推荐:使用 Objects.equals(JDK7+)
import java.util.Objects;
// ✅ 安全:自动处理 null
boolean eq = Objects.equals(s1, s2); // s1 和 s2 任一为 null 均返回 false
✅ 推荐:封装工具方法(适用于复杂业务)
public static boolean safeEquals(Object a, Object b) {
if (a == b) return true; // 同为 null 或同一引用
if (a == null || b == null) return false;
return a.equals(b);
}
自定义类中 equals 的 null 处理规范
在重写 equals 时,务必在方法开头处理 null:
equals 实现public class User {
private String id;
private String name;
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // 自反性
if (obj == null || getClass() != obj.getClass()) return false; // null 或类型不同
User user = (User) obj;
return Objects.equals(id, user.id) && Objects.equals(name, user.name);
}
}
先判断 obj == null,再判断 getClass() != obj.getClass(),可避免子类对象误判为相等(破坏对称性)。若允许子类相等(如 Date 与子类),应使用 instanceof 并谨慎设计逻辑。
equals 与 hashCode:成对出现的黄金搭档
“这个关系贼关键。你想想,当你 HashSet 里存一个对象时,它靠的是 hashCode 先去快排,再用 equals 去最终比对。”
HashSet.add(obj) 时,先调用 obj.hashCode() 确定桶位置。
若桶中已有对象,再调用 equals 比较是否重复。
set.contains(obj) 时,同样先算哈希值定位桶,再用 equals 确认。
违反契约的灾难性后果
equals 而未重写 hashCodeSet set = new HashSet<>();
User u1 = new User("001", "Alice");
User u2 = new User("001", "Alice");
set.add(u1);
set.add(u2); // 本应去重,但因 hashCode 不同,实际存入两个对象!
System.out.println(set.size()); // 输出 2(错误!)
这就是“哈希失效”——对象在逻辑上相等,却因哈希值不同被误判为不同元素。
hashCode 的设计原则
- 一致性:对象未修改时,多次调用
hashCode()应返回相同值。 - 相等性:若
a.equals(b)为true,则a.hashCode() == b.hashCode()必须为true。 - 不强制唯一性:不相等的对象可有相同哈希值(哈希冲突),但应尽量分散以提升性能。
使用 Objects.hash 简化实现
hashCodeimport java.util.Objects;
public class User {
private String id;
private String name;
@Override
public boolean equals(Object obj) {
// ...(同上)
}
@Override
public int hashCode() {
return Objects.hash(id, name); // 自动处理 null,组合字段哈希
}
}
Objects.hash 内部使用 Arrays.hashCode,对多个字段进行组合哈希,避免手动计算溢出或分布不均。若字段较多,可考虑使用 Lombok 的 @EqualsAndHashCode 注解自动生成。
自定义类重写 equals:完整实践指南
“那到底啥时候该用 equals,啥时候该 ==?好办粗暴的回答是:别为了省事直接 ==。”
重写 equals 的标准步骤
- 检查引用:若
this == obj,返回true。 - 检查 null:若
obj == null,返回false。 - 检查类型:若
getClass() != obj.getClass(),返回false(推荐用instanceof仅在允许子类相等时)。 - 字段比较:逐个比较关键字段(使用
Objects.equals处理null)。 - 返回结果:若所有字段相等,返回
true。
经典案例:自定义 Point 类
equals 与 hashCodeimport java.util.Objects;
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Point point = (Point) obj;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
经典案例:自定义 User 类(含 null 安全)
import java.util.Objects;
public class User {
private String id; // 主键,唯一标识
private String name;
private Integer age;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
User user = (User) obj;
return Objects.equals(id, user.id); // 仅用 id 比较(主键相等即逻辑相等)
}
@Override
public int hashCode() {
return Objects.hash(id); // 仅用 id 计算哈希
}
}
业务中应优先使用“业务唯一标识”作为 equals 和 hashCode 的依据(如数据库主键 id),而非业务字段(如 name 可能重复或变更)。这能确保对象在持久化前后逻辑一致性。
经典案例:自定义 DateRange 类(时间区间)
import java.util.Objects;
public class DateRange {
private final LocalDate start;
private final LocalDate end;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
DateRange that = (DateRange) obj;
return Objects.equals(start, that.start) && Objects.equals(end, that.end);
}
@Override
public int hashCode() {
return Objects.hash(start, end);
}
}
实战场景:常见业务逻辑中的 equals 应用
集合去重:避免重复订单
场景:用户提交订单时,需防止重复提交相同订单号的请求。
Order 对象去重Set orderSet = new HashSet<>();
Order order1 = new Order("ORD001", "iPhone", 1);
Order order2 = new Order("ORD001", "iPhone", 1);
orderSet.add(order1);
orderSet.add(order2); // 若 Order 正确重写了 equals/hashCode,实际仅存一个
System.out.println(orderSet.size()); // 输出 1(正确去重)
数据库同步:比对本地与远程数据
场景:从数据库查出两个 Product 对象,需判断是否为同一商品。
Product local = productDao.findById(123);
Product remote = remoteService.getProduct(123);
if (local.equals(remote)) {
System.out.println("数据一致,无需更新");
} else {
System.out.println("数据不一致,触发同步");
}
状态机流转:判断状态是否变化
场景:订单状态变更时,需判断新旧状态是否不同。
OrderStatus oldStatus = order.getStatus();
OrderStatus newStatus = updateStatus(order);
if (!oldStatus.equals(newStatus)) {
// 触发状态变更通知
notifyStatusChange(order, oldStatus, newStatus);
}
缓存穿透:缓存中查找对象
场景:从缓存中获取用户信息,需判断缓存键是否命中。
Objects.equals)String cacheKey = "user:" + userId;
User cachedUser = cache.get(cacheKey);
if (Objects.equals(cachedUser.getId(), userId)) {
return cachedUser;
} else {
// 缓存未命中或键冲突,重新查询
return loadUserFromDB(userId);
}
常见问题解答(FAQ)
为什么 String 的 equals 比较内容,而 Object 默认比较地址?
String 类重写了 equals 方法,使其基于字符序列比较。这是 Java 的设计选择——字符串常用于逻辑比较,因此默认行为符合直觉。
重写 equals 后必须重写 hashCode 吗?
是的!这是 Java 规范的硬性要求。违反会导致集合类行为异常(如 HashSet 存入重复元素)。
子类能重写父类的 equals 吗?
可以,但需谨慎。若子类添加新字段,可能破坏对称性(如父类相等但子类不等)。建议使用 instanceof 并仅比较父类字段,或使用组合而非继承。
Lombok 的 @EqualsAndHashCode 安全吗?
基本安全,但默认包含所有非静态字段。若字段包含集合或复杂对象,建议显式指定 of 或 exclude,并确保逻辑符合业务需求。
网友们还关心
- Java 8 中
equals有什么新特性? → 无语法变更,但Objects工具类更易用。 equals和compareTo有什么区别? →equals判断相等性,compareTo判断排序顺序(需实现Comparable)。- 为什么
Double的equals对0.0和-0.0返回false? → 因 IEEE 754 规范中二者哈希值不同,符合契约。
? 网友们还关心
以下问题均来自开发者社区高频讨论,与 java中equals怎么用 - Java 中 equals 常用用法 高度相关:
- “为什么
new Integer(1).equals(new Integer(1))返回true,但new Integer(128).equals(new Integer(128))也返回true?” →Integer重写了equals,比较数值而非引用。 - “
LocalDate的equals是基于年月日比较吗?” → 是的,且已正确重写,可安全用于集合。 - “自定义类重写
equals后,Jackson 序列化会受影响吗?” → 不会,序列化默认不调用equals。