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);
    }
}

方法逻辑:

  1. 若引用相同(==比较),直接返回true
  2. 若对象为null或类型不同,返回false
  3. 逐个比较关键属性

修改测试验证结果:

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

关键实践建议:

  1. 对象比较时,优先使用Objects.equals()避免NPE
  2. 重写equals()时必须同时重写hashCode()
  3. 基本类型与null比较时直接使用==(编译器会阻止非法操作)

示例源码可在GitHub仓库获取。


原始标题:Difference Between == and equals() in Java