Компаратор и сопоставимость в Java

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 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) in the type Collections is not applicable for the arguments (ArrayList)

Давайте разберемся, что мы здесь сделали не так.

3. Сопоставимый

Как следует из названия, Comparable - это интерфейс, определяющий стратегию сравнения объекта с другими объектами того же типа. Это называется «естественным порядком».

Соответственно, чтобы иметь возможность сортировать - мы должны определить наш объект Player как сопоставимый, реализовав интерфейс Comparable :

public class Player implements Comparable { // same as before @Override public int compareTo(Player otherPlayer) { return Integer.compare(getRanking(), otherPlayer.getRanking()); } } 

Порядок сортировки определяется возвращаемым значением метода compareTo () . Integer.compare (х, у) возвращает -1 , если х меньше у , возвращает 0 , если они равны, и возвращает 1 в противном случае.

Метод возвращает число, показывающее, является ли сравниваемый объект меньше, равен или больше, чем объект, переданный в качестве аргумента.

Наконец, когда мы запускаем наш PlayerSorter , мы можем видеть наших игроков, отсортированных по их рейтингу:

Before Sorting : [John, Roger, Steven] After Sorting : [Steven, John, Roger]

Теперь, когда у нас есть четкое понимание естественного упорядочения в Comparable , давайте посмотрим, как мы можем использовать другие типы упорядочения более гибким образом, чем прямая реализация интерфейса.

4. Компаратор

Интерфейс Comparator определяет метод compare (arg1, arg2) с двумя аргументами, которые представляют сравниваемые объекты, и работает аналогично методу Comparable.compareTo () .

4.1. Создание компараторов

Чтобы создать компаратор, мы должны реализовать интерфейс компаратора .

В нашем первом примере мы создадим Comparator, чтобы использовать атрибут рейтинга Player для сортировки игроков:

public class PlayerRankingComparator implements Comparator { @Override public int compare(Player firstPlayer, Player secondPlayer) { return Integer.compare(firstPlayer.getRanking(), secondPlayer.getRanking()); } }

Точно так же мы можем создать Comparator, чтобы использовать атрибут age Player для сортировки игроков:

public class PlayerAgeComparator implements Comparator { @Override public int compare(Player firstPlayer, Player secondPlayer) { return Integer.compare(firstPlayer.getAge(), secondPlayer.getAge()); } }

4.2. Компараторы в действии

Чтобы продемонстрировать концепцию, давайте изменим наш 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 предоставляет новые способы определения компараторов с помощью лямбда-выражений и статического фабричного метода comparing () .

Давайте посмотрим на быстрый пример того, как использовать лямбда-выражение для создания компаратора :

Comparator byRanking = (Player player1, Player player2) -> Integer.compare(player1.getRanking(), player2.getRanking());

Метод Comparator.comparing принимает метод, вычисляющий свойство, которое будет использоваться для сравнения элементов, и возвращает соответствующий экземпляр Comparator :

Comparator byRanking = Comparator .comparing(Player::getRanking); Comparator byAge = Comparator .comparing(Player::getAge);

Вы можете подробно изучить функциональность Java 8 в нашем руководстве по сравнению с Java 8 Comparator.

5. Компаратор против сопоставимого

Интерфейс Comparable - хороший выбор, когда он используется для определения порядка по умолчанию или, другими словами, если это основной способ сравнения объектов.

Затем мы должны спросить себя, зачем использовать компаратор, если у нас уже есть Comparable ?

Причин несколько:

  • Sometimes, we can't modify the source code of the class whose objects we want to sort, thus making the use of Comparable impossible
  • Using Comparators allows us to avoid adding additional code to our domain classes
  • We can define multiple different comparison strategies which isn't possible when using Comparable

6. Avoiding the Subtraction Trick

Over the course of this tutorial, we used the Integer.compare() method to compare two integers. One might argue that we should use this clever one-liner instead:

Comparator comparator = (p1, p2) -> p1.getRanking() - p2.getRanking();

Although it's much more concise compared to other solutions, it can be a victim of integer overflows in Java:

Player player1 = new Player(59, "John", Integer.MAX_VALUE); Player player2 = new Player(67, "Roger", -1); List players = Arrays.asList(player1, player2); players.sort(comparator);

Since -1 is much less than the Integer.MAX_VALUE, “Roger” should come before the “John” in the sorted collection. However, due to integer overflow, the “Integer.MAX_VALUE – (-1)” will be less than zero. So, based on the Comparator/Comparable contract, the Integer.MAX_VALUE is less than -1, which is obviously incorrect.

Hence, despite what we expected, “John” comes before the “Roger” in the sorted collection:

assertEquals("John", players.get(0).getName()); assertEquals("Roger", players.get(1).getName());

7. Conclusion

In this tutorial, we explored the Comparable and Comparator interfaces and discussed the differences between them.

To understand more advanced topics of sorting, check out our other articles such as Java 8 Comparator, Java 8 Comparison with Lambdas.

И, как обычно, исходный код можно найти на GitHub.