1. 概述
作为 Java 开发者,我们经常需要对集合中的元素进行排序。Java 支持我们使用各种排序算法来处理任意类型的数据。
例如,我们可以按字母顺序、逆序或者长度对字符串进行排序。
在本篇文章中,我们将深入探讨 Comparable
接口及其核心方法 compareTo
的使用,它为对象的排序提供了基础支持。我们会分别介绍如何对 Java 核心类和自定义类的对象进行排序。
同时,我们还会提到实现 compareTo
方法时需要遵循的规则,以及一些应避免的常见错误模式。
2. Comparable 接口
Comparable
接口的作用是 为每个实现它的类的对象强加一种自然排序规则。
compareTo
是 Comparable
接口中唯一定义的方法,通常被称为“自然比较方法”。
2.1. 实现 compareTo 方法
compareTo
方法用于 将当前对象与传入的另一个对象进行比较。
在实现时,需要确保该方法返回:
- ✅ 正整数:如果当前对象“大于”传入对象
- ❌ 负整数:如果当前对象“小于”传入对象
- ⚠️ 零:如果两个对象“相等”
这个逻辑在数学上被称为符号函数(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))
此外,强烈建议(但非强制)保持 compareTo
与 equals
方法的一致性:
x.compareTo(y) == 0
应与x.equals(y)
返回相同的布尔值
这可以确保对象在 TreeSet
和 TreeMap
中表现一致。
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.sort
或 Arrays.sort
对实现了 Comparable
接口的对象进行排序。
3.1. Java 核心类
Java 核心类如 String
、Integer
、Double
等都已实现了 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
TreeMap
和 TreeSet
是 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
方法。
我们也演示了对核心类和自定义类的排序操作,并探讨了在 TreeSet
和 TreeMap
中使用时的注意事项。
最后,我们介绍了 Comparator
接口的使用场景,它是自定义排序逻辑的强大工具。
一如既往,本文中的示例代码可以在 GitHub 上找到。