在Java编程中,判断两个对象或值是否相等是一个基础且核心的操作,许多初学者甚至一些有经验的开发者都可能对其中涉及的细节存在误解,导致代码出现难以排查的bug,要准确判断Java中的相等性,需要区分两种基本操作:运算符和equals()方法,并理解它们在不同场景下的行为差异,对于基本数据类型和引用数据类型,这两种方式的处理方式也截然不同。

基本数据类型的比较
对于Java中的八种基本数据类型(byte, short, int, long, float, double, char, boolean),判断相等性的操作非常直接,在这些类型上,运算符会比较它们的“值”是否相等,也就是说,如果两个基本类型变量的存储值相同,那么表达式的结果就是true。
int a = 10; int b = 10; System.out.println(a == b); // 输出 true
在这个例子中,变量a和b都存储了整数值10,因此a == b的比较结果为true,这种比较是基于值的,不涉及任何对象的概念,同样,对于其他基本类型也是如此,比如两个double类型的变量只要数值相同(不考虑NaN的情况),就会返回true,需要注意的是,对于浮点数类型(float和double),由于浮点数在计算机中的表示方式以及精度问题,直接使用进行比较有时可能会得到意想不到的结果,在涉及高精度计算时,通常推荐使用一个很小的误差范围(epsilon)来判断两个浮点数是否“足够接近”,而不是要求它们完全相等。
引用数据类型的比较与运算符
当涉及到引用数据类型(即所有类的实例)时,运算符的行为发生了根本性的变化,在引用类型上,比较的是两个引用变量是否指向“同一个内存地址”,也就是判断它们是否是同一个对象的实例,这被称为“引用相等”或“身份相等”(identity equality)。
考虑以下代码:
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2); // 输出 false
这里,我们创建了两个String对象,尽管它们的内容都是”hello”,但new关键字为它们在内存中分配了两个不同的空间,因此str1和str2指向的是两个不同的对象实例。str1 == str2的比较结果是false,因为它们的内存地址不同,这与基本类型的比较形成了鲜明对比。
一个常见的特例是字符串字面量,Java为了优化性能,会将对字符串字面量的引用指向同一个内存地址(字符串常量池)。

String str3 = "hello"; String str4 = "hello"; System.out.println(str3 == str4); // 输出 true
在这个例子中,str3和str4都指向字符串常量池中的同一个”hello”对象,所以比较返回true,这再次说明了在引用类型上比较的是地址,而不是内容。
equals()方法:逻辑相等的判断
既然在引用类型上无法满足我们比较对象内容的需求,Java提供了equals()方法。equals()方法在Object类中定义,其默认实现与运算符完全相同,也是比较两个对象的内存地址,许多类,特别是所有以数据为中心的类,都重写(override)了equals()方法,以实现基于内容的、有意义的逻辑相等(logical equality)判断。
以String类为例,它重写了equals()方法,使得该方法会比较两个字符串对象的内容(即字符序列)是否相同:
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1.equals(str2)); // 输出 true
这次,我们使用equals()方法,比较的是两个字符串对象内部的字符序列,结果自然是true,几乎所有Java标准库中的类,如Integer, Double, Date, List等,都提供了自己的equals()实现,以便进行符合业务逻辑的比较。
自定义类中正确实现equals()方法
当我们编写自定义类时,如果需要比较两个实例的逻辑相等性,就必须重写equals()方法,一个糟糕的equals()实现会导致意想不到的错误,特别是在将对象放入HashSet或作为HashMap的键时,根据Object类的通用约定,一个正确的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和y所包含的信息没有被修改,多次调用x.equals(y)必须一致地返回true或false。
一个实现equals()方法的经典“模板”如下:

@Override
public boolean equals(Object obj) {
// 1. 检查是否是同一个对象实例
if (this == obj) {
return true;
}
// 2. 检查obj是否为null,或者是否属于不同的类
if (obj == null || getClass() != obj.getClass()) {
return false;
}
// 3. 类型转换,将obj转换成当前类的类型
MyClass other = (MyClass) obj;
// 4. 比较类的各个字段,注意,对于引用类型字段,也应使用equals()方法。
// 使用java.util.Objects.equals()可以避免处理null指针问题。
return Objects.equals(this.field1, other.field1) &&
this.field2 == other.field2; // 基本类型直接用==
}
在这个模板中,我们首先检查是否为同一个对象,然后检查类型是否匹配,最后逐一比较关键字段,比较字段时,基本类型使用,引用类型使用Objects.equals()或重写后的equals()方法,这样可以安全地处理null值。
equals()与hashCode()的契约关系
在重写equals()方法的同时,几乎总是需要重写hashCode()方法,Java有一个约定:如果两个对象根据equals()方法判断是相等的,那么调用它们的hashCode()方法必须产生相同的整数结果,反之则不成立:两个对象的hashCode()相同,它们不一定相等(哈希碰撞),这个约定至关重要,因为违反它会导致基于哈希的集合类(如HashMap, HashSet, Hashtable)无法正常工作,如果一个对象被用作HashMap的键,并且它的hashCode()在对象被放入集合后发生了变化,那么这个键将再也无法被找到。
在Java中判断相等性,需要根据操作数的类型和业务需求做出正确选择:
- 对于基本数据类型:始终使用运算符,它直接比较变量的值。
- 对于引用数据类型:
- 如果需要判断两个引用是否指向内存中的同一个对象,使用运算符。
- 如果需要判断两个对象在逻辑上是否“相等”(即内容相同),应使用
equals()方法。
- 对于自定义类:如果需要逻辑相等的判断,必须重写
equals()方法,并严格遵守其自反性、对称性、传递性和一致性约定,为了确保在哈希集合中的正确性,也必须重写hashCode()方法,并遵守equals()与hashCode()之间的契约。
理解并正确应用和equals()的区别,是编写健壮、无bug Java代码的基石,通过区分“引用相等”和“逻辑相等”,我们可以精确地表达程序中的相等性判断意图,避免因混淆概念而导致的常见错误。



















