1.概述
作为Java开发人员,我们经常需要对集合在一起的元素进行排序。 Java允许我们对任何类型的数据实现各种排序算法。
例如,我们可以按字母顺序,反向字母顺序或基于长度对字符串排序。
在本教程中,我们将探讨Comparable
接口及其compareTo
方法,该方法可以进行排序。我们将研究包含核心和自定义类中的对象的排序集合。
我们还将提及正确实现compareTo
规则,以及需要避免的损坏模式。
2.Comparable接口
Comparable
接口对实现它的每个类的对象强加排序。
compareTo
是Comparable
接口定义的唯一方法。它通常被称为自然比较法。
2.1。实现compareTo
compareTo
方法将当前对象与作为参数发送的对象进行比较。
在实现它时,我们需要确保该方法返回:
- 如果当前对像大于参数对象,则为正整数
- 一个负整数,如果当前对像小于参数对象
- 如果当前对像等于参数对象,则为零
在数学中,我们称其为符号或符号函数:
2.2。示例实施
让我们看一下如何在核心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();
但是,整数不足以存储差值,因此给我们错误的结果。当然,此模式由于可能的整数溢出而被破坏,需要避免。
正确的解决方案是使用比较而不是减法。我们还可以重用核心Integer
类中的正确实现:
@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(e2) == 0
应该具有与x.equals(y)
相同的布尔值
这将确保我们可以安全地在已排序的集合和已排序的地图中使用对象。
2.5。 equals
一致性
让我们看看当compareTo
和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);
}
在排序集或排序映射中使用此类时,可能会导致意外的行为:
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);
排序的集合使用compareTo
而不是equals
方法执行所有元素比较。因此,从其角度看,这两个玩家似乎是等效的,并且不会添加第二个玩家。
3.排序集合
Comparable
接口的主要目的是使对集合或数组中的元素进行自然排序。
我们可以使用Java实用程序方法Collections.sort
或Arrays.sort
对实现Comparable
所有对象进行Arrays.sort
。
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
的对象的集合进行排序,则Java编译器将引发错误。
如果我们对数组尝试相同的操作,则编译期间不会失败。但是,这将导致类强制转换运行时异常:
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 Collections Framework的两个实现,可帮助我们对它们的元素进行自动排序。
我们可以使用在排序映射中或在排序集中的元素中实现Comparable
接口的对象。
让我们看一个自定义类的示例,该类根据球员得分的目标比较球员:
@Override
public int compareTo(FootballPlayer anotherPlayer) {
return Integer.compare(this.goalsScored, anotherPlayer.goalsScored);
}
在我们的示例中,键是根据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);
当我们不想或无法修改要排序的对象的源代码时,这通常也是一个不错的选择。
5.结论
在本文中,我们研究了如何使用Comparable
接口为Java类定义自然排序算法。我们研究了一个常见的损坏模式,并定义了如何正确实现compareTo
方法。
我们还探讨了同时包含核心和自定义类的排序集合。接下来,我们考虑了在排序集和排序映射中使用的类中compareTo
方法的实现。
最后,我们研究了一些应该使用Comparator
接口的用例。
0 评论