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