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
— это интерфейс, определяющий стратегию сравнения объекта с другими объектами того же типа. Это называется «естественным упорядочением» класса.
Чтобы иметь возможность сортировать, мы должны определить наш объект Player
как сопоставимый, реализовав интерфейс Comparable
:
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)
возвращает -1, если x
меньше y
, 0, если они равны, и 1 в противном случае.
Метод возвращает число, указывающее, меньше, равно или больше сравниваемый объект, чем объект, передаваемый в качестве аргумента.
Теперь, когда мы запускаем наш PlayerSorter
, мы можем видеть наших игроков
, отсортированных по их рейтингу:
Before Sorting : [John, Roger, Steven]
After Sorting : [Steven, John, Roger]
Теперь, когда у нас есть четкое представление о естественном упорядочении с помощью Comparable
, давайте посмотрим, как мы можем использовать другие типы упорядочения более гибким образом, чем путем прямой реализации интерфейса.
4. Компаратор
Интерфейс Comparator
определяет метод compare(arg1, arg2)
с двумя аргументами, представляющими сравниваемые объекты, и работает аналогично методу Comparable.compareTo()
.
4.1. Создание компараторов
Чтобы создать Comparator,
мы должны реализовать интерфейс 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. Компараторы
в действии
Чтобы продемонстрировать концепцию, давайте изменим наш 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]
Если нам нужен другой порядок сортировки, нам нужно только изменить используемый компаратор
:
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 предоставляет новые способы определения компараторов
с помощью лямбда-выражений и статического фабричного метода сравнения() .
Давайте посмотрим на краткий пример того, как использовать лямбда-выражение для создания 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. Компаратор
против сравнимого
Интерфейс Comparable
— хороший выбор для определения порядка по умолчанию или, другими словами, если это основной способ сравнения объектов.
Так зачем использовать Comparator
, если у нас уже есть Comparable
?
Есть несколько причин, почему:
- Иногда мы не можем изменить исходный код класса, объекты которого мы хотим отсортировать, что делает невозможным использование
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
, «Роджер» должен стоять перед «Джоном» в отсортированной коллекции. Однако из-за целочисленного переполнения «Integer.MAX_VALUE — (-1)»
будет меньше нуля . Таким образом, на основе контракта Comparator/
Comparable Integer.MAX_VALUE
меньше -1, что явно неверно.
Поэтому, несмотря на то, что мы ожидали, «Джон» стоит перед «Роджером» в отсортированном наборе:
assertEquals("John", players.get(0).getName());
assertEquals("Roger", players.get(1).getName());
7. Заключение
В этой статье мы рассмотрели интерфейсы Comparable
и Comparator
и обсудили различия между ними.
Чтобы понять более сложные темы сортировки, ознакомьтесь с другими нашими статьями, такими как Компаратор Java 8 и Сравнение Java 8 с Lambdas .
Как обычно, исходный код можно найти на GitHub .