1. 概述
本教程将介绍Java中两种核心的相等性检查机制——引用相等和值相等。我们将通过对比分析、代码示例和关键差异说明,帮助开发者彻底理解这两种检查方式。
特别地,我们会深入探讨null
检查场景,并解释为什么在处理对象时应该优先使用引用相等而非值相等。
2. 引用相等
首先理解引用比较,它通过相等运算符(==
)实现。当两个引用指向内存中的同一个对象时,即发生引用相等。
2.1. 基本类型的相等运算符
Java基本类型是简单的非类原始值。使用相等运算符比较基本类型时,实际比较的是它们的值:
int a = 10;
int b = 15;
assertFalse(a == b);
int c = 10;
assertTrue(a == c);
int d = a;
assertTrue(a == d);
如上所示,对于基本类型,相等性和引用检查效果完全一致。当用相同值初始化新变量时,检查返回true
。将原值重新赋给新变量再比较,结果同样成立。
尝试null
检查时:
int e = null; // 编译错误
assertFalse(a == null); // 编译错误
assertFalse(10 == null); // 编译错误
Java禁止将null
赋给基本类型。基本类型变量或值无法使用相等运算符进行任何null
检查。
2.2. 对象类型的相等运算符
对于Java对象类型,相等运算符仅执行引用相等性比较,完全忽略对象内容。先创建一个简单自定义类:
public class Person {
private String name;
private int age;
// 构造函数、getter、setter...
}
初始化对象并观察比较结果:
Person a = new Person("Bob", 20);
Person b = new Person("Mike", 40);
assertFalse(a == b);
Person c = new Person("Bob", 20);
assertFalse(a == c);
Person d = a;
assertTrue(a == d);
结果与基本类型截然不同。第二次检查返回false
(基本类型中为true
)。如前所述,相等运算符比较时忽略对象内部值,仅检查两个变量是否引用同一内存地址。
与基本类型不同,对象可以处理null
:
assertFalse(a == null);
Person e = null;
assertTrue(e == null);
通过相等运算符与null
比较,本质是检查变量是否已初始化。
3. 值相等
现在聚焦值相等性测试。当两个独立对象具有相同值或状态时,即发生值相等。
这种比较与Object的equals()方法密切相关。下面分别分析其在基本类型和对象类型上的应用差异。
3.1. 基本类型的equals()方法
基本类型是单一值的简单类型,不实现任何方法。因此无法直接调用equals()方法:
int a = 10;
assertTrue(a.equals(10)); // 编译错误
但每个**基本类型都有对应的包装类**,可通过自动装箱机制转换:
int a = 10;
Integer b = a;
assertTrue(b.equals(10));
3.2. 对象类型的equals()方法
回到Person类。要使equals()方法正确工作,需在自定义类中重写该方法,考虑类中所有字段:
public class Person {
// 其他字段和方法省略
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
}
方法逻辑:
- 若引用相同(
==
比较),直接返回true
- 若对象为
null
或类型不同,返回false
- 逐个比较关键属性
修改测试验证结果:
Person a = new Person("Bob", 20);
Person b = new Person("Mike", 40);
assertFalse(a.equals(b));
Person c = new Person("Bob", 20);
assertTrue(a.equals(c));
Person d = a;
assertTrue(a.equals(d));
第二次检查返回true
(引用相等为false
),证明重写的equals()方法正确比较了对象内容。
若不重写equals(),将使用Object类的默认实现(仅检查引用相等),可能导致不符合预期的结果。
注意:重写equals()时必须同时重写hashCode(),以确保方法间的一致性。
4. Null相等性
最后分析equals()方法处理null
的场景:
Person a = new Person("Bob", 20);
Person e = null;
assertFalse(a.equals(e));
assertThrows(NullPointerException.class, () -> e.equals(a));
根据调用顺序不同,结果迥异:
- 非null对象调用equals(null) → 返回
false
- null对象调用equals() → 抛出NullPointerException
修复方案:先进行引用检查:
assertFalse(e != null && e.equals(a));
利用短路特性,左侧为false
时右侧不执行,避免NPE。务必确保调用equals()的对象不为null,否则可能引发隐藏bug。
Java 7+ 提供了更优雅的解决方案——null安全的Objects.equals():
assertFalse(Objects.equals(e, a));
assertTrue(Objects.equals(null, e));
此方法内部处理了null检查,当两参数均为null时返回true
。
5. 结论
本文深入分析了Java中引用相等与值相等的实现机制:
引用相等检查
- 使用
==
运算符 - 基本类型:比较值
- 对象类型:比较内存地址
- 与
null
比较:检查对象是否已初始化
值相等检查
- 使用equals()方法
- 基本类型:需先装箱为包装类
- 对象类型:需正确重写equals()方法
- 调用equals()前必须确保对象非null
关键实践建议:
- 对象比较时,优先使用Objects.equals()避免NPE
- 重写equals()时必须同时重写hashCode()
- 基本类型与null比较时直接使用
==
(编译器会阻止非法操作)
示例源码可在GitHub仓库获取。