Java 中的 Comparator 和 Comparable
1. 简介
Java 中的比较非常容易,直到事实并非如此。
当使用自定义类型或尝试比较无法直接比较的对象时,我们需要使用比较策略。我们可以通过使用 Comparator或Comparable接口来构建一个比较策略。
在 Java 中,Comparator
和 Comparable
都是用于比较对象大小的接口,但它们的使用场景和实现方式有所不同:
1. Comparable 接口
- 作用: 允许类自身定义比较逻辑,以便在需要排序时进行比较。
- 实现: 类实现
Comparable
接口,并重写compareTo(Object o)
方法。该方法接受一个对象作为参数,并返回一个整数:- 返回 0:表示两个对象相等
- 返回负数:表示当前对象小于参数对象
- 返回正数:表示当前对象大于参数对象
- 使用: 在使用
Arrays.sort()
或Collections.sort()
等排序方法时,如果要排序的类实现了Comparable
接口,则会自动调用compareTo()
方法进行比较。
2. Comparator 接口
- 作用: 提供一种外部的、可定制的比较逻辑,允许根据不同的需求对对象进行排序。
- 实现: 实现
Comparator
接口,并重写compare(Object o1, Object o2)
方法。该方法接受两个对象作为参数,并返回一个整数:- 返回 0:表示两个对象相等
- 返回负数:表示第一个对象小于第二个对象
- 返回正数:表示第一个对象大于第二个对象
- 使用: 可以将
Comparator
对象传递给排序方法,例如Arrays.sort(array, comparator)
或Collections.sort(list, comparator)
,以指定排序规则。
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() ** 方法的返回值决定 。 如果 x 小于 y ,则 Integer.compare (x, y) 返回 -1 ,如果相等,则返回 0,否则返回 1。**
该方法返回一个数字,指示被比较的对象是否小于、等于或大于作为参数传递的对象。
现在,当我们运行PlayerSorter时,我们可以看到我们的玩家按他们的排名排序:
Before Sorting : [John, Roger, Steven]
After Sorting : [Steven, John, Roger]
现在我们已经清楚地了解了Comparable的自然排序,让我们看看如何以比直接实现接口更灵活的方式使用其他类型的排序。
4.Comparator
**Comparator接口定义了一个compare(arg1, arg2) 方法,该 **方法具有两个表示比较对象的参数,其工作方式与Comparable.compareTo() 方法类似。
4.1. 创建比较器
要创建Comparator, 我们必须实现Comparator接口。
对于我们的第一个例子,我们将创建一个比较器来使用Player的排名属性对玩家进行排序:**
public class PlayerRankingComparator implements Comparator<Player> {
@Override
public int compare(Player firstPlayer, Player secondPlayer) {
return Integer.compare(firstPlayer.getRanking(), secondPlayer.getRanking());
}
}
同样的,我们可以创建一个Comparator,使用Player的年龄属性对球员进行排序:**
public class PlayerAgeComparator implements Comparator<Player> {
@Override
public int compare(Player firstPlayer, Player secondPlayer) {
return Integer.compare(firstPlayer.getAge(), secondPlayer.getAge());
}
}
4.2. Comparator的作用
为了演示这个概念,让我们通过向Collections.sort方法引入第二个参数来修改我们的PlayerSorter , 这实际上是我们要使用的Comparator的实例。******
使用这种方法,我们可以覆盖自然顺序:
PlayerRankingComparator playerComparator = new PlayerRankingComparator();
Collections.sort(footballTeam, playerComparator);
现在让我们运行PlayerRankingSorter 来查看结果:
Before Sorting : [John, Roger, Steven]
After Sorting by ranking : [Steven, John, Roger]
如果我们想要不同的排序顺序,我们只需要改变我们正在使用的比较器:
PlayerAgeComparator playerComparator = new PlayerAgeComparator();
Collections.sort(footballTeam, playerComparator);
现在,当我们运行PlayerAgeSorter时,我们可以看到按年龄排列的不同顺序 :
Before Sorting : [John, Roger, Steven]
After Sorting by age : [Roger, John, Steven]
4.3. Java 8Comparator
Java 8通过使用 lambda 表达式和Comparative() 静态工厂方法提供了定义比较器的新方法。**
让我们看一个简单的例子来了解如何使用 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);
5. Comparable与Comparator
Comparable接口对于定义默认顺序是一个不错的选择, 或者换句话说,如果它是比较对象的主要方式。
那么如果我们已经有了Comparable ,为什么还要使用 Comparator 呢?**
原因如下:
- 有时我们无法修改要排序的对象的类的源代码,因此无法使用Comparable
- 使用比较器可以让我们避免在域类中添加额外的代码
- 我们可以定义多种不同的比较策略,而使用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 Comparator和Java 8 与 Lambdas 的比较。
转载自:https://juejin.cn/post/7391694596722114587