在 Java 开发中,比较对象或值是否相等是高频操作,而 == 和 equals() 是两种核心比较方式。很多开发者初期会混淆二者的用法,尤其在自定义对象中重写 equals() 后,常常忽略 hashCode() 的重写,最终引发潜在 Bug。本文将从“二者核心区别”“equals() 底层原理”“重写配对的原因”三个维度,帮你彻底理清逻辑,规避开发陷阱。
== 是 Java 中的运算符,其比较逻辑根据操作数的类型不同而有所差异,核心分为“基本数据类型”和“引用数据类型”两类场景:
对于 byte、short、int、long、float、double、char、boolean 这 8 种基本数据类型,== 直接比较变量存储的“具体数值”,与内存地址无关。因为基本类型的值存储在栈中(局部变量)或堆中(成员变量),不存在“引用”概念,比较逻辑直观。
public class CompareDemo {
public static void main(String[] args) {
int a = 100;
int b = 100;
byte c = 100;
// 基本类型比较值,100 == 100,结果为 true
System.out.println(a == b); // true
// int 和 byte 自动类型转换后值相等,结果为 true
System.out.println(a == c); // true
}
}对于类、接口、数组等引用类型,变量存储的是“对象在堆内存中的地址引用”,而非对象本身。此时 == 比较的是两个变量是否指向同一个对象(即地址是否相同),而非对象的内容是否相等。
public class CompareDemo {
public static void main(String[] args) {
String s1 = new String("Java");
String s2 = new String("Java");
String s3 = s1;
// s1 和 s2 是两个不同对象,地址不同,结果为 false
System.out.println(s1 == s2); // false
// s3 指向 s1 对应的对象,地址相同,结果为 true
System.out.println(s1 == s3); // true
}
}这里需要注意 String 常量池的特殊情况:若通过String s4 = "Java" 方式创建对象,会优先从常量池获取实例,此时 s4 == s1 仍为 false(s1 是堆中新建对象),但 s4 == "Java" 为 true(复用常量池实例),本质还是地址比较。
equals() 是 java.lang.Object 类中的方法,所有 Java 类都默认继承该方法,其核心逻辑与 == 绑定,但可通过重写实现自定义比较规则。
Object 类中 equals() 的源码如下,可见其本质就是通过== 比较对象地址,即判断两个变量是否指向同一个对象。
public boolean equals(Object obj) {
return (this == obj);
}这意味着,若自定义类未重写 equals(),调用该方法时,效果与 == 完全一致,比较的是对象地址。
实际开发中,我们往往需要判断“两个对象的内容是否相等”(而非是否是同一个对象),此时就需要重写 equals() 方法。例如 String、Integer 等 JDK 内置类,都已重写 equals(),实现了基于内容的比较。
以 String 类为例,其重写的 equals() 会逐字符比较字符串内容,只有所有字符都相同,才返回 true:
// String 类重写的 equals() 核心逻辑(简化版)
public boolean equals(Object anObject) {
if (this == anObject) {
return true; // 地址相同直接返回 true,优化性能
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) { // 逐字符比较内容
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}自定义类重写 equals() 的示例(以 User 类为例,判断 id 和 name 都相同则视为相等):
public class User {
private Integer id;
private String name;
// 构造方法、getter/setter 省略
@Override
public boolean equals(Object o) {
if (this == o) return true; // 地址相同,直接返回 true
if (o == null || getClass() != o.getClass()) return false; // 空判断、类型判断
User user = (User) o;
// 自定义比较规则:id 和 name 都相等则视为对象相等
return Objects.equals(id, user.id) && Objects.equals(name, user.name);
}
}这里用到 Objects.equals() 而非直接 ==,是为了避免空指针异常(若 id 为 null,直接调用 id.equals() 会报错)。
这是面试高频考点,核心原因是要遵守 equals() 和 hashCode() 的“通用约定”——该约定是 Java 集合框架(如 HashMap、HashSet)正常工作的基础,若违反约定,会导致集合操作出现异常。
hashCode() 也是 Object 类的方法,返回一个 int 类型的哈希值,其核心作用是“为集合提供快速定位依据”。例如 HashMap 中,会先通过对象的哈希值确定存储位置(桶位),再通过 equals() 精确比较,从而大幅提升查询效率。
Java 官方对 equals() 和 hashCode() 制定了以下核心约定(缺一不可):
equals() 比较相等,则它们的hashCode() 必须返回相同的值。hashCode() 返回相同的值,它们的 equals() 不一定相等(哈希冲突场景)。equals() 比较不相等,它们的 hashCode() 可以相同,也可以不同(但不同能减少哈希冲突,提升集合效率)。若只重写 equals() 而不重写 hashCode(),会违反“约定 1”,导致依赖哈希值的集合(HashMap、HashSet 等)出现逻辑错误。
举个反例(基于上文 User 类,仅重写 equals() 未重写 hashCode()):
public class HashCodeDemo {
public static void main(String[] args) {
User u1 = new User(1, "张三");
User u2 = new User(1, "张三");
// equals() 重写后,内容相等,返回 true
System.out.println(u1.equals(u2)); // true
// 未重写 hashCode(),默认返回对象地址对应的哈希值,u1 和 u2 地址不同,哈希值不同
System.out.println(u1.hashCode()); // 123456(示例值)
System.out.println(u2.hashCode()); // 654321(示例值)
// 将两个对象放入 HashSet(HashSet 不允许重复元素,依赖 hashCode() 和 equals())
Set<User> set = new HashSet<>();
set.add(u1);
set.add(u2);
// 预期:set 中只有 1 个元素(u1 和 u2 相等)
// 实际:set 中有 2 个元素(哈希值不同,被视为两个不同元素)
System.out.println(set.size()); // 2
}出现该问题的原因的是:HashSet 添加元素时,先通过 hashCode() 计算存储位置,若该位置无元素,则直接存入;若有元素,再通过 equals() 比较是否重复。本例中 u1 和 u2 的哈希值不同,被分配到不同位置,即使 equals() 相等,也被视为两个不同元素,违背了 HashSet“去重”的核心逻辑。
重写 hashCode() 时,需保证“equals() 相等的对象,哈希值一定相等”,通常会基于 equals() 中用到的字段(如 User 类的 id、name)计算哈希值,确保逻辑一致。
手动重写 hashCode() 需注意哈希值的均匀性(减少冲突),推荐使用 Objects.hash()方法(JDK 7+),它会自动处理字段空值,且能基于多个字段生成哈希值,简洁又安全。
完善 User 类,同步重写 equals() 和 hashCode():
import java.util.Objects;
public class User {
private Integer id;
private String name;
// 构造方法、getter/setter 省略
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
// 基于 id 和 name 比较内容
return Objects.equals(id, user.id) && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
// 基于 id 和 name 生成哈希值,与 equals() 逻辑一致
return Objects.hash(id, name);
}
}此时再执行上文 HashSet 示例,u1 和 u2 的哈希值相同,且 equals() 相等,HashSet 会判定为重复元素,最终 set 大小为 1,符合预期。
这是最常见的错误,本质是违反约定,会导致集合操作异常,解决方式就是同步重写两个方法,基于相同字段计算逻辑。
哈希冲突是不可避免的(不同对象可能生成相同哈希值),因此 hashCode() 相等仅代表“可能相等”,最终需通过 equals() 确认,这也是 HashMap 等集合的核心工作逻辑。
若 hashCode() 返回随机值,会导致同一对象每次调用 hashCode() 结果不同,放入 HashMap 后无法被正常查询(定位不到原存储位置),需基于对象的稳定字段(如 id、name)生成哈希值。
==,直接比较值,高效且无歧义。equals(),若需判断“是否是同一个对象”,再用 ==。equals() 和 hashCode(),且基于相同字段逻辑实现,推荐用 Objects 工具类简化代码。equals() 和 hashCode() 符合约定,否则会出现添加重复元素、查询不到元素等问题。== 是运算符,基本类型比“值”,引用类型比“地址”;equals() 是方法,未重写时等价于 ==,重写后可实现“内容比较”。equals() 必须重写 hashCode(),核心是遵守 Java 官方约定,确保“equals() 相等的对象,hashCode() 必相等”,否则会导致哈希集合逻辑异常。Objects.equals() 和 Objects.hash() 重写,兼顾空指针安全、逻辑一致性和哈希均匀性,规避常见陷阱。(注:文档部分内容可能由 AI 生成)原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。