Java中的Comparator和Comparable
1.绪论
在 Java 中进行比较非常容易,除非它们不是。
当使用自定义类型时,或者试图比较那些不能直接比较的对象时,我们需要使用一个比较策略。我们可以通过使用Comparator或Comparable接口简单地建立一个。
2.设置实例
让我们举一个足球队的例子,我们想按排名排列球员。
我们将从创建一个简单的Player类开始:
public class Player {
private int ranking;
private String name;
private int age;
// constructor, getters, setters
}
接下来,我们将创建一个PlayerSorter类来创建我们的集合,并尝试使用Collections.sort对它进行排序:
public static void main(String[] args) {
List<Player> footballTeam = new ArrayList<>();
Player player1 = new Player(59, "John", 20);
Player player2 = new Player(67, "Roger", 22);
Player player3 = new Player(45, "Steven", 24);
footballTeam.add(player1);
footballTeam.add(player2);
footballTeam.add(player3);
System.out.println("Before Sorting : " + footballTeam);
Collections.sort(footballTeam);
System.out.println("After Sorting : " + footballTeam);
}
正如预期的那样,这导致了一个编译时错误:
The method sort(List<T>) in the type Collections
is not applicable for the arguments (ArrayList<Player>)
现在,让我们试着了解我们在这里做错了什么。
3.Comparable
顾名思义,Comparable是一个接口,定义了将一个对象与其他同类对象进行比较的策略。这被称为类的“自然排序”。
为了能够进行排序,我们必须通过实现Comparable接口将我们的Player对象定义为可比较的对象:
public class Player implements Comparable<Player> {
// same as before
@Override
public int compareTo(Player otherPlayer) {
return Integer.compare(getRanking(), otherPlayer.getRanking());
}
}
排序顺序由compareTo()方法的返回值决定。Integer.compare(x, y)如果x 小于y则返回-1,如果它们相等则返回0,否则返回1。
该方法返回一个数字,表明被比较的对象是否小于、等于或大于作为参数传递的对象。
现在,当我们运行我们的PlayerSorter时,我们可以看到我们的Players按照他们的排名进行排序:
Before Sorting : [John, Roger, Steven]
After Sorting : [Steven, John, Roger]
现在我们已经清楚地了解了使用Comparable的自然排序,让我们看看我们如何以更灵活的方式使用其他类型的排序,而不是直接实现一个接口。
4.Comparator
Comparator 接口定义了一个compare(arg1, arg2) 方法,它有两个参数,代表被比较的对象,其工作方式类似于Comparable.compareTo() 方法。
4.1.创建Comparator
要创建一个 Comparator,我们必须实现 Comparator 接口。
对于我们的第一个例子,我们将创建一个Comparator 来使用Player的ranking属性来对球员进行排序:
public class PlayerRankingComparator implements Comparator<Player> {
@Override
public int compare(Player firstPlayer, Player secondPlayer) {
return Integer.compare(firstPlayer.getRanking(), secondPlayer.getRanking());
}
}
同样地,我们可以创建一个 Comparator 来使用Player的 age 属性来对球员进行排序:
public class PlayerAgeComparator implements Comparator<Player> {
@Override
public int compare(Player firstPlayer, Player secondPlayer) {
return Integer.compare(firstPlayer.getAge(), secondPlayer.getAge());
}
}
4.2.Comparator 的使用
为了证明这个概念,让我们修改我们的PlayerSorter,为Collections.sort 方法引入第二个参数,这实际上是我们要使用的Comparator的实例。
使用这种方法,我们可以覆盖自然的排序:
PlayerRankingComparator playerComparator = new PlayerRankingComparator();
Collections.sort(footballTeam, playerComparator);
现在,让我们运行我们的PlayerRankingSorter来看看结果:
Before Sorting : [John, Roger, Steven]
After Sorting by ranking : [Steven, John, Roger]
如果我们想要一个不同的排序顺序,我们只需要改变我们所使用的Comparator即可:
PlayerAgeComparator playerComparator = new PlayerAgeComparator();
Collections.sort(footballTeam, playerComparator);
现在,当我们运行我们的PlayerAgeSorter时,我们可以看到不同的排序顺序,按年龄:。
Before Sorting : [John, Roger, Steven]
After Sorting by age : [Roger, John, Steven]
4.3. Java 8 比较器
Java 8通过使用lambda表达式和comparing()静态工厂方法,为定义Comparators提供了新的方法。
让我们看一个快速的例子,看看如何使用lambda表达式来创建一个Comparator:
Comparator byRanking =
(Player player1, Player player2) -> Integer.compare(player1.getRanking(), player2.getRanking());
Comparator.comparing方法接收一个计算将用于比较项目的属性的方法,并返回一个匹配的Comparator实例:
Comparator<Player> byRanking = Comparator
.comparing(Player::getRanking);
Comparator<Player> byAge = Comparator
.comparing(Player::getAge);
要深入探讨 Java 8 的功能,请查看我们的 Java 8 Comparator.comparing 指南。
5.Comparator与Comparable对比
Comparable接口是用来定义默认排序的好选择,或者换句话说,如果它是比较对象的主要方式的话。
那么,如果我们已经有了Comparable,为什么还要使用Comparator呢?
这有几个原因:
- 有时我们不能修改我们想要排序的对象的类的源代码,从而使得Comparable的使用变得不可能
- 使用Comparator可以让我们避免在领域类中添加额外的代码
- 我们可以定义多个不同的比较策略,这在使用Comparable的时候是不可能的。
6.避免减法技巧
在本教程中,我们已经使用Integer.compare()方法来比较两个整数。然而,有人可能会说,我们应该用这个聪明的单行代码来代替:
Comparator<Player> comparator = (p1, p2) -> p1.getRanking() - p2.getRanking();
尽管它比其他解决方案要简洁得多,但它可能是Java中整数溢出的受害者:
Player player1 = new Player(59, "John", Integer.MAX_VALUE);
Player player2 = new Player(67, "Roger", -1);
List<Player> players = Arrays.asList(player1, player2);
players.sort(comparator);
由于 -1 远小于 Integer.MAX_VALUE,因此“Roger”在排序集合中应该排在“John”之前。 但是,由于整数溢出,“Integer.MAX_VALUE – (-1)”将小于零。因此,根据 Comparator/Comparable 约定,Integer.MAX_VALUE 小于 -1,这显然是不正确的。
因此,尽管我们预料到了,但在排序的集合中,“John”排在了“Roger”之前:
assertEquals("John", players.get(0).getName());
assertEquals("Roger", players.get(1).getName());
7.结语
在这篇文章中,我们探讨了Comparable和Comparator接口,并讨论了它们之间的不同之处。
要了解排序的更多高级主题,请查看我们的其他文章,如Java 8比较器,以及Java 8用Lambdas进行比较。
像往常一样,源代码可以在GitHub上找到。