Перейти к основному содержимому

Компаратор и Comparable в Java

· 5 мин. чтения

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 .