跳至主要內容

18. Java 比较对象

LiuSongLing大约 8 分钟javajava

比较对象是面向对象编程语言的基本特征。

在本教程中,我们将探索Java语言的一些功能,这些功能允许我们比较对象。我们还将研究外部库中的此类功能。

1.==和!=

让我们从 == 和开始 != 运算符,可以分别判断两个Java对象是否相同。

对于原始类型,相同意味着具有相等的值:

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

多亏了自动开箱,这在将原始值与其包装类型对应值进行比较时也有效:







Integer a = new Integer(1);
assertThat(1 == a).isTrue();

如果两个整数的值不同,==运算符将返回false,而!=运算符将返回true。

2.对象

假设我们想比较两种具有相同值的整数包装类型:

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

assertThat(a == b).isFalse();

通过比较两个对象,这些对象的值不是1。相反,它们在堆栈中的内存地址是不同的,因为两个对象都是使用 new 运算符创建的。如果我们把a分配给b,那么我们会有不同的结果:

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

在这种情况下,它们被认为是相同的。这是因为valueOf()方法将整数存储在缓存中,以避免创建太多具有相同值的包装对象。因此,该方法为两个调用返回相同的整数实例。







Java也为字符串这样做:

assertThat("Hello!" == "Hello!").isTrue();

然而,如果它们是使用新运算符创建的,那么它们就不一样了。

最后,两个空引用被认为是相同的,而任何非空对象都被认为与空不同:

assertThat(null == null).isTrue();

assertThat("Hello!" == null).isFalse();

当然,相等运算符的行为可能是有限的。如果我们想比较映射到不同地址的两个对象,但根据其内部状态将它们视为相等呢?我们将在接下来的章节中看到如何做到这一点。

2.Object#equals方法

现在让我们用equals()方法谈谈更广泛的平等概念。







此方法在Object类中定义,以便每个Java对象都继承它。默认情况下,它的实现比较对象内存地址,因此它的工作原理与==运算符相同。然而,我们可以覆盖此方法,以定义平等对我们的对象意味着什么。

首先,让我们看看它对像Integer这样的现有对象的行为:

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

assertThat(a.equals(b)).isTrue();

当两个对象相同时,该方法仍然返回true。

我们应该注意,我们可以将空对象作为方法的参数传递,但不能作为我们调用方法的对象。

我们也可以将equals()方法与我们自己的对象一起使用。假设我们有一个人类:

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

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

我们可以覆盖该类的equals()方法,以便我们可以根据两个人的内部详细信息进行比较:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    PersonWithEquals that = (PersonWithEquals) o;
    return firstName.equals(that.firstName) &&
      lastName.equals(that.lastName);
}

3.对象#equals 静态方法

现在让我们看看Objects#equals静态方法。我们之前提到,我们不能使用null作为第一个对象的值,否则将抛出NullPointerException。







Objects helper类的equals()方法解决了这个问题。它需要两个参数并进行比较,也处理空值。

让我们再次比较Person对象:

PersonWithEquals joe = new PersonWithEquals("Joe", "Portman");
PersonWithEquals joeAgain = new PersonWithEquals("Joe", "Portman");
PersonWithEquals natalie = new PersonWithEquals("Natalie", "Portman");

assertThat(Objects.equals(joe, joeAgain)).isTrue();
assertThat(Objects.equals(joe, natalie)).isFalse();

正如我们所解释的,此方法处理空值。因此,如果两个参数都是空的,它将返回true,如果只有一个参数是空的,它将返回false。

这真的很方便。假设我们想在我们的Person类中添加一个可选的出生日期:

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

然后我们必须更新我们的equals()方法,但使用空处理。我们可以通过将条件添加到ourequals()方法来做到这一点:

birthDate == null ? that.birthDate == null : birthDate.equals(that.birthDate);

然而,如果我们在类中添加太多可为空的字段,它可能会变得非常混乱。在我们的equals()实现中使用Objects#equals方法要干净得多,并提高了可读性:

Objects.equals(birthDate, that.birthDate);

4.compareTo接口

比较逻辑也可用于将对象按特定顺序放置。可比接口允许我们通过确定一个对象是否大于、相等或小于另一个对象来定义对象之间的顺序。

可比较接口是通用的,只有一个方法,compareTo(),它接受泛型类型的参数并返回一个int。如果返回值低于参数,则为负值;如果等于,则返回值为0,否则为正值。







假设,在我们的Person类中,我们想按姓氏比较Person对象:

public class PersonWithEqualsAndComparable implements Comparable<PersonWithEqualsAndComparable> {
    //...

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

如果使用姓氏大于此的人调用,compareTo()方法将返回负int,如果姓氏相同,则返回零,否则返回正。

5.Comparator接口

比较器接口是通用的,有一个比较方法,该方法接受该通用类型的两个参数并返回一个整数。我们之前已经在可比界面中看到了这种模式。

比较器是相似的;然而,它与类的定义是分开的。因此,我们可以为一个类定义尽可能多的比较器,我们只能提供一个可比的实现。

让我们想象一下,我们有一个在表格视图中显示人员的网页,我们希望为用户提供按名字而不是姓氏进行排序的可能性。如果我们也想保持当前的实现,这在Comparable中是不可能的,但我们可以实现我们自己的比较器。

让我们创建一个仅按名字进行比较的人比较器:

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







现在,让我们对使用该比较器的人进行排序:

Person joe = new Person("Joe", "Portman");
Person allan = new Person("Allan", "Dale");

List<Person> people = new ArrayList<>();
people.add(joe);
people.add(allan);

people.sort(compareByFirstNames);

assertThat(people).containsExactly(allan, joe);

在比较器接口上,我们还可以在 comparing 实现中使用其他方法:

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

在这种情况下,我们首先比较姓氏,然后比较名字。接下来,我们比较出生日期,但由于它们是无效的,我们必须说如何处理,即应该根据其自然顺序对它们进行比较,零值排在最后。

6.Apache Commmon库

首先,让我们导入Maven依赖项:

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

6.1 ObjectUtils#notEqual方法

首先,让我们谈谈ObjectUtils#notEqual方法。根据它们自己的equals()方法实现,需要两个对象参数来确定它们是否不相等。它还处理空值。

让我们重用我们的字符串示例:

String a = new String("Hello!");
String b = new String("Hello World!");

assertThat(ObjectUtils.notEqual(a, b)).isTrue();

应该注意的是,ObjectUtils有一个equals()方法。然而,自Java 7以来,Objects#equals出现后,这就被弃用了

6.2 ObjectUtils#compare方法

现在让我们用ObjectUtils#compare方法比较对象顺序。这是一个泛型方法,它接受该泛型类型的两个可比参数,并返回一个整数。

让我们再次使用字符串来查看它:







String first = new String("Hello!");
String second = new String("How are you?");

assertThat(ObjectUtils.compare(first, second)).isNegative();

默认情况下,该方法通过考虑更大的空值来处理空值。它还提供了一个重载版本,提供反转该行为,并认为它们较小,采用布尔参数。

6.Google guava库

首先,让我们导入依赖项:

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

谷歌为我们提供了一种方法来确定两个对象是否相等,Objects#equal:

String a = new String("Hello!");
String b = new String("Hello!");

assertThat(Objects.equal(a, b)).isTrue();

guava库提供了ComparisonChain类,允许我们通过比较链比较两个对象。我们可以通过名字和姓氏轻松比较两个Person对象:

Person natalie = new Person("Natalie", "Portman");
Person joe = new Person("Joe", "Portman");

int comparisonResult = ComparisonChain.start()
  .compare(natalie.getLastName(), joe.getLastName())
  .compare(natalie.getFirstName(), joe.getFirstName())
  .result();

assertThat(comparisonResult).isPositive();