1. 简介

对象比较是面向对象编程语言中的核心能力之一。在 Java 中,我们有多种方式来判断两个对象是否“相等”或如何排序。本文将系统梳理 Java 原生支持的比较机制,并对比 Apache Commons 和 Guava 等主流第三方库的增强方案,帮助你在实际开发中避免踩坑。

2. == 和 != 操作符

==!= 是 Java 中最基础的比较操作符,用于判断两个引用是否指向同一个对象。

2.1 基本类型

对于基本类型(如 int, boolean),== 比较的是值是否相等

assertThat(1 == 1).isTrue();

得益于自动拆箱(auto-unboxing),基本类型和其包装类之间的比较也能正常工作:

Integer a = new Integer(1);
assertThat(1 == a).isTrue(); // ✅ 自动拆箱后比较值

2.2 引用类型

对于对象(引用类型),== 比较的是内存地址(引用)是否相同,而不是对象内容。

来看一个经典踩坑场景:

Integer a = new Integer(1);
Integer b = new Integer(1);

assertThat(a == b).isFalse(); // ❌ 两个 new 出来的对象,地址不同

即使值相同,new 操作符会创建两个独立的对象,因此 == 返回 false

但如果是指向同一对象的引用:

Integer a = new Integer(1);
Integer b = a;

assertThat(a == b).isTrue(); // ✅ 同一个引用

再看一个利用缓存的特例 —— Integer.valueOf()

Integer a = Integer.valueOf(1);
Integer b = Integer.valueOf(1);

assertThat(a == b).isTrue(); // ✅ 缓存了 -128~127 范围内的 Integer

Integer.valueOf() 内部有缓存池,小整数会复用同一个实例,因此 == 成立。

字符串字面量也有类似机制:

assertThat("Hello!" == "Hello!").isTrue(); // ✅ 字符串常量池

但通过 new String() 创建的字符串不会进入常量池:

String a = new String("Hello!");
String b = new String("Hello!");
assertThat(a == b).isFalse(); // ❌ 两个独立对象

最后,null 的比较规则:

assertThat(null == null).isTrue();         // ✅ null 和 null 相等
assertThat("Hello!" == null).isFalse();    // ❌ 非 null 和 null 不相等

⚠️ 小结:== 仅比较引用,不适合用于内容比较。要比较对象内容,必须使用 equals()

3. Object#equals 方法

equals() 是定义“逻辑相等性”的标准方式。它定义在 Object 类中,所有类都继承该方法。

默认行为

默认实现等价于 ==,即比较内存地址:

public boolean equals(Object obj) {
    return (this == obj);
}

重写 equals()

为了实现基于内容的比较,我们需要重写 equals() 方法。

Person 类为例:

public class PersonWithEquals {
    private String firstName;
    private String lastName;

    public PersonWithEquals(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                    // ✅ 引用相同,直接返回 true
        if (o == null || getClass() != o.getClass()) return false; // ✅ 类型检查
        PersonWithEquals that = (PersonWithEquals) o;
        return firstName.equals(that.firstName) &&
               lastName.equals(that.lastName);         // ✅ 字段逐个比较
    }
}

✅ 重写要点:

  • 先判断 this == o,提升性能
  • 判断 null 和类型是否匹配
  • 强制类型转换后逐字段比较

测试效果:

PersonWithEquals a = new PersonWithEquals("Joe", "Portman");
PersonWithEquals b = new PersonWithEquals("Joe", "Portman");

assertThat(a.equals(b)).isTrue(); // ✅ 内容相同即相等

⚠️ 注意:重写 equals() 时通常也需要重写 hashCode(),以满足 Java 的通用约定。

4. Objects#equals 静态方法

Objects.equals(a, b) 是 Java 7 引入的工具方法,用于安全地比较两个对象,自动处理 null

优势

相比直接调用 a.equals(b)Objects.equals() 更安全:

// ❌ 可能抛出 NullPointerException
assertThat(a.equals(b)).isTrue();

// ✅ 安全,推荐使用
assertThat(Objects.equals(a, b)).isTrue();

行为规则:

  • Objects.equals(null, null)true
  • Objects.equals(null, "abc")false
  • Objects.equals("abc", "abc")true

实际应用

当类中包含可空字段时,Objects.equals() 能极大简化 equals() 方法的编写:

public class PersonWithBirthDate extends PersonWithEquals {
    private LocalDate birthDate;

    public PersonWithBirthDate(String firstName, String lastName, LocalDate birthDate) {
        super(firstName, lastName);
        this.birthDate = birthDate;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PersonWithBirthDate that = (PersonWithBirthDate) o;
        return super.equals(o) &&
               Objects.equals(birthDate, that.birthDate); // ✅ 简洁安全
    }
}

对比手动处理 null 的繁琐写法:

// ❌ 冗长且易出错
birthDate == null ? that.birthDate == null : birthDate.equals(that.birthDate);

5. Comparable 接口

Comparable 用于定义对象的自然排序,适用于类本身有明确排序逻辑的场景。

接口定义

public interface Comparable<T> {
    int compareTo(T o);
}

返回值含义:

  • 负数:this < o
  • 0:this == o
  • 正数:this > o

实现示例

Person 按姓氏(lastName)排序:

public class PersonWithEqualsAndComparable implements Comparable<PersonWithEqualsAndComparable> {
    private String firstName;
    private String lastName;

    // 构造方法...

    @Override
    public int compareTo(PersonWithEqualsAndComparable o) {
        return this.lastName.compareTo(o.lastName);
    }
}

测试:

PersonWithEqualsAndComparable a = new PersonWithEqualsAndComparable("Joe", "Doe");
PersonWithEqualsAndComparable b = new PersonWithEqualsAndComparable("Joe", "Smith");

assertThat(a.compareTo(b)).isNegative(); // ✅ Doe < Smith

⚠️ 一个类只能实现一个 Comparable,无法灵活切换排序规则。

6. Comparator 接口

Comparator 是更灵活的排序工具,与类定义解耦,支持多种排序策略。

核心优势

  • ✅ 一个类可以有多个 Comparator
  • ✅ 无需修改原类
  • ✅ 支持链式比较

常见用法

创建按名字排序的 Comparator

Comparator<Person> compareByFirstNames = Comparator.comparing(Person::getFirstName);

对列表排序:

List<Person> people = Arrays.asList(
    new Person("Joe", "Portman"),
    new Person("Allan", "Dale")
);

people.sort(compareByFirstNames);

assertThat(people).containsExactly(
    new Person("Allan", "Dale"),
    new Person("Joe", "Portman")
);

链式比较(thenComparing)

构建复合排序规则,例如:先按姓,再按名,最后按生日(null 放最后):

@Override
public int compareTo(Person o) {
    return Comparator.comparing(Person::getLastName)
                     .thenComparing(Person::getFirstName)
                     .thenComparing(Person::getBirthDate, 
                                    Comparator.nullsLast(Comparator.naturalOrder()))
                     .compare(this, o);
}

✅ 这种写法简洁、可读性强,是现代 Java 推荐的风格。

7. Apache Commons

Apache Commons Lang 是一个广泛使用的工具库,提供了丰富的对象操作工具。

7.1 Maven 依赖

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

7.2 ObjectUtils#notEqual

判断两个对象是否不相等,安全处理 null

String a = "Hello";
String b = "World";

assertThat(ObjectUtils.notEqual(a, b)).isTrue();  // ✅ 不相等
assertThat(ObjectUtils.notEqual(a, a)).isFalse(); // ✅ 相等

⚠️ 注意:ObjectUtils.equals() 已被标记为过时,建议使用 Objects.equals()

7.3 ObjectUtils#compare

比较两个 Comparable 对象的顺序:

String first = "Apple";
String second = "Banana";

assertThat(ObjectUtils.compare(first, second)).isNegative(); // ✅ Apple < Banana

支持 null 处理策略:

  • 默认:null 被视为最大值
  • 可通过重载方法设置 null 为最小值

8. Guava

Google Guava 提供了更现代、函数式风格的工具方法。

8.1 Maven 依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.0.1-jre</version>
</dependency>

8.2 Objects#equal

Guava 的 Objects.equal(a, b)Objects.equals(a, b) 功能一致:

assertThat(Objects.equal("a", "a")).isTrue();   // ✅ 相等
assertThat(Objects.equal(null, null)).isTrue(); // ✅ null 安全

⚠️ 尽管未标记为 @Deprecated,但官方文档建议优先使用 JDK 自带的 Objects.equals()

8.3 原始类型比较

Guava 为基本类型提供了 compare() 方法:

assertThat(Ints.compare(1, 2)).isNegative();     // ✅ 1 < 2
assertThat(Longs.compare(100L, 50L)).isPositive(); // ✅ 100 > 50

覆盖 int, long, float, double, short, char, boolean

8.4 ComparisonChain

链式比较神器,代码简洁且高效:

int result = ComparisonChain.start()
    .compare(person1.getLastName(), person2.getLastName())
    .compare(person1.getFirstName(), person2.getFirstName())
    .compare(person1.getAge(), person2.getAge())
    .result();

// result < 0: person1 < person2
// result = 0: 相等
// result > 0: person1 > person2

✅ 优点:

  • 一旦某次比较得出非零结果,后续比较将短路,性能好
  • 语法直观,适合复杂排序逻辑

9. 总结

方式 适用场景 是否推荐
== 引用比较 ⚠️ 仅用于基本类型或引用判等
equals() 内容相等性 ✅ 必须重写
Objects.equals() 安全的 equals 比较 ✅ 推荐替代直接调用 equals
Comparable 自然排序 ✅ 单一排序逻辑
Comparator 多种排序策略 ✅ 推荐,尤其是链式比较
Apache Commons 旧项目兼容 ⚠️ 新项目优先用 JDK
Guava 函数式风格 ✅ 特别是 ComparisonChain

✅ 最佳实践:

  • 重写 equals() 时务必重写 hashCode()
  • 使用 Objects.equals() 处理可空字段
  • 排序优先使用 Comparator.comparing().thenComparing() 链式调用
  • 复杂排序可考虑 ComparisonChain

示例代码已托管至 GitHub:https://github.com/your-repo/java-comparison-examples


原始标题:Comparing Objects in Java