1. 概述

作为 Java 开发者,我们经常需要对集合中的元素进行排序。Java 支持我们使用各种排序算法来处理任意类型的数据

例如,我们可以按字母顺序、逆序或者长度对字符串进行排序。

在本篇文章中,我们将深入探讨 Comparable 接口及其核心方法 compareTo 的使用,它为对象的排序提供了基础支持。我们会分别介绍如何对 Java 核心类和自定义类的对象进行排序。

同时,我们还会提到实现 compareTo 方法时需要遵循的规则,以及一些应避免的常见错误模式。

2. Comparable 接口

Comparable 接口的作用是 为每个实现它的类的对象强加一种自然排序规则

compareToComparable 接口中唯一定义的方法,通常被称为“自然比较方法”。

2.1. 实现 compareTo 方法

compareTo 方法用于 将当前对象与传入的另一个对象进行比较

在实现时,需要确保该方法返回:

  • ✅ 正整数:如果当前对象“大于”传入对象
  • ❌ 负整数:如果当前对象“小于”传入对象
  • ⚠️ 零:如果两个对象“相等”

这个逻辑在数学上被称为符号函数(signum function):

Signum function

2.2. 示例实现

我们来看一下 Java 核心类 Integer 是如何实现 compareTo 的:

@Override
public int compareTo(Integer anotherInteger) {
    return compare(this.value, anotherInteger.value);
}

public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

2.3. 错误的减法模式

有些人可能会想用一行减法代码来实现:

@Override
public int compareTo(BankAccount anotherAccount) {
    return this.balance - anotherAccount.balance;
}

考虑下面这个例子,我们期望一个正余额账户“大于”负余额账户:

BankAccount accountOne = new BankAccount(1900000000);
BankAccount accountTwo = new BankAccount(-2000000000);
int comparison = accountOne.compareTo(accountTwo);
assertThat(comparison).isNegative();

但由于整型溢出,这个减法结果是错误的。❌ 这种模式是错误的,必须避免

✅ 正确的做法是使用比较逻辑而不是减法。也可以复用 Java 核心类的实现:

@Override
public int compareTo(BankAccount anotherAccount) {
    return Integer.compare(this.balance, anotherAccount.balance);
}

2.4. 实现规则

为了正确实现 compareTo 方法,必须遵守以下数学规则:

  • sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
  • (x.compareTo(y) > 0 && y.compareTo(z) > 0) 暗示 x.compareTo(z) > 0
  • x.compareTo(y) == 0 暗示 sgn(x.compareTo(z)) == sgn(y.compareTo(z))

此外,强烈建议(但非强制)保持 compareToequals 方法的一致性

  • x.compareTo(y) == 0 应与 x.equals(y) 返回相同的布尔值

这可以确保对象在 TreeSetTreeMap 中表现一致。

2.5. 与 equals 的一致性

来看一个不一致的例子。假设 compareTo 比较进球数,而 equals 比较球员姓名:

@Override
public int compareTo(FootballPlayer anotherPlayer) {
    return this.goalsScored - anotherPlayer.goalsScored;
}

@Override
public boolean equals(Object object) {
    if (this == object)
        return true;
    if (object == null || getClass() != object.getClass())
        return false;
    FootballPlayer player = (FootballPlayer) object;
    return name.equals(player.name);
}

这会导致 TreeSet 中出现意外行为:

FootballPlayer messi = new FootballPlayer("Messi", 800);
FootballPlayer ronaldo = new FootballPlayer("Ronaldo", 800);

TreeSet<FootballPlayer> set = new TreeSet<>();
set.add(messi);
set.add(ronaldo);

assertThat(set).hasSize(1);
assertThat(set).doesNotContain(ronaldo);

因为 TreeSet 是基于 compareTo 判断重复的,所以这两个进球数相同的球员会被视为重复项,导致 ronaldo 未被加入。

3. 对集合进行排序

Comparable 接口的主要作用是 为集合或数组中的元素提供自然排序能力

我们可以使用 Java 提供的工具类方法 Collections.sortArrays.sort 对实现了 Comparable 接口的对象进行排序。

3.1. Java 核心类

Java 核心类如 StringIntegerDouble 等都已实现了 Comparable 接口。

因此,对它们排序非常简单,直接复用其自然排序逻辑即可。

例如,对整数数组按自然顺序排序(升序):

int[] numbers = new int[] {5, 3, 9, 11, 1, 7};
Arrays.sort(numbers);
assertThat(numbers).containsExactly(1, 3, 5, 7, 9, 11);

字符串的自然排序则是按字母顺序:

String[] players = new String[] {"ronaldo", "modric", "ramos", "messi"};
Arrays.sort(players);
assertThat(players).containsExactly("messi", "modric", "ramos", "ronaldo");

3.2. 自定义类

对于自定义类来说,如果要支持排序,必须手动实现 Comparable 接口。

如果尝试对未实现 Comparable 的对象集合排序,编译器会报错;如果是数组,则在运行时抛出 ClassCastException

HandballPlayer duvnjak = new HandballPlayer("Duvnjak", 197);
HandballPlayer hansen = new HandballPlayer("Hansen", 196);
HandballPlayer[] players = new HandballPlayer[] {duvnjak, hansen};
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> Arrays.sort(players));

3.3. TreeMap 和 TreeSet

TreeMapTreeSet 是 Java 集合框架中支持自动排序的两个类。

它们可以自动对实现了 Comparable 接口的对象进行排序。

示例:按进球数排序球员:

@Override
public int compareTo(FootballPlayer anotherPlayer) {
    return Integer.compare(this.goalsScored, anotherPlayer.goalsScored);
}

TreeMap 中,键会自动按 compareTo 定义的规则排序:

FootballPlayer ronaldo = new FootballPlayer("Ronaldo", 900);
FootballPlayer messi = new FootballPlayer("Messi", 800);
FootballPlayer modric = new FootballPlayer("modric", 100);

Map<FootballPlayer, String> players = new TreeMap<>();
players.put(ronaldo, "forward");
players.put(messi, "forward");
players.put(modric, "midfielder");

assertThat(players.keySet()).containsExactly(modric, messi, ronaldo);

4. 使用 Comparator 作为替代方案

除了自然排序,Java 还允许我们通过灵活的方式定义自定义排序逻辑。

Comparator 接口支持为对象定义多个不同的比较策略,且无需修改对象本身的代码:

FootballPlayer ronaldo = new FootballPlayer("Ronaldo", 900);
FootballPlayer messi = new FootballPlayer("Messi", 800);
FootballPlayer modric = new FootballPlayer("Modric", 100);

List<FootballPlayer> players = Arrays.asList(ronaldo, messi, modric);
Comparator<FootballPlayer> nameComparator = Comparator.comparing(FootballPlayer::getName);
Collections.sort(players, nameComparator);

assertThat(players).containsExactly(messi, modric, ronaldo);

当你无法或不想修改类源码时,Comparator 是一个非常实用的选择。

5. 总结

本文中我们深入讲解了如何使用 Comparable 接口为 Java 类定义自然排序逻辑,指出了一个常见的错误实现方式,并介绍了如何正确实现 compareTo 方法。

我们也演示了对核心类和自定义类的排序操作,并探讨了在 TreeSetTreeMap 中使用时的注意事项。

最后,我们介绍了 Comparator 接口的使用场景,它是自定义排序逻辑的强大工具。

一如既往,本文中的示例代码可以在 GitHub 上找到。


原始标题:Guide to Implementing the compareTo Method | Baeldung