1. Введение
В жизни редко бывает достаточно одного способа сравнивать объекты. Представьте, что у вас есть список пользователей: иногда вы хотите сортировать их по имени, иногда — по возрасту, а иногда — по длине фамилии. Или у вас есть класс, который вообще не вы — автор, и добавить в него compareTo не получится. Именно для таких случаев в Java существует интерфейс Comparator.
Когда Comparable не хватает
- Класс нельзя менять (например, он из сторонней библиотеки).
- Нужно несколько способов сортировки (по разным полям).
- Хотите отделить логику сравнения от самого класса (например, сортировать по-разному в разных частях программы).
Аналогия
Если Comparable — это встроенный «естественный порядок» объекта, то Comparator — это внешний судья, который может оценивать ваши объекты по любым критериям: сегодня по имени, завтра — по возрасту, послезавтра — по длине имени.
2. Интерфейс Comparator: синтаксис и контракт
Объявление интерфейса
public interface Comparator<T> {
int compare(T o1, T o2);
}
Метод compare должен возвращать:
- Отрицательное число, если первый объект «меньше» второго.
- 0, если они равны.
- Положительное число, если первый «больше» второго.
Контракт тот же, что и у Comparable, только теперь сравниваются два объекта, а не «текущий» и «другой» через compareTo.
Пример: компаратор для сортировки по фамилии
Допустим, у нас есть класс Person:
public class Person {
private String firstName;
private String lastName;
private int age;
// Конструктор и геттеры
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
}
Создадим компаратор, который будет сортировать по фамилии:
import java.util.Comparator;
public class LastNameComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return a.getLastName().compareTo(b.getLastName());
}
}
Заметка: метод compareTo у строк (String) сравнивает их в алфавитном порядке.
3. Использование Comparator: сортировка коллекций
Сортировка с помощью компаратора
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Анна", "Костецкая", 25));
people.add(new Person("Борис", "Новак", 20));
people.add(new Person("Виктория", "Белл", 22));
// Сортировка по фамилии
Collections.sort(people, new LastNameComparator());
for (Person p : people) {
System.out.println(p.getLastName() + " " + p.getFirstName());
}
}
}
Результат:
Новак Борис
Костецкая Анна
Белл Виктория
Сортировка по возрасту с помощью компаратора
Даже если класс уже реализует Comparable по имени, можно создать отдельный компаратор — по возрасту:
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return Integer.compare(a.getAge(), b.getAge());
}
}
И использовать аналогично:
Collections.sort(people, new AgeComparator());
Результат:
Борис Новак (20)
Виктория Белл (22)
Анна Костецкая (25)
Пример: выбор компаратора «на лету»
Collections.sort(people, new LastNameComparator()); // По фамилии
Collections.sort(people, new AgeComparator()); // По возрасту
4. Анонимные классы и лямбда-выражения
Компараторы можно создавать «на лету», не объявляя отдельных классов.
Анонимный класс
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getFirstName().compareTo(b.getFirstName());
}
});
Лямбда-выражение
Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));
Или ещё короче с методом списка List.sort:
people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
- Анонимные классы — старый способ, громоздко.
- Лямбда — современно и компактно.
5. Примеры: сортировка по разным критериям
Сортировка по длине фамилии
Comparator<Person> byLastNameLength = (a, b) ->
Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);
Сортировка по возрасту, потом по имени (многоуровневая)
Comparator<Person> byAgeThenName = (a, b) -> {
int cmp = Integer.compare(a.getAge(), b.getAge());
if (cmp != 0) return cmp;
return a.getFirstName().compareTo(b.getFirstName());
};
people.sort(byAgeThenName);
Использование компаратора для поиска (пример)
Компаратор применим не только к сортировке, но и к поиску в отсортированных коллекциях:
// people должны быть отсортированы по возрасту!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
System.out.println("Найден человек с возрастом 22: " + people.get(idx));
}
6. Best practices и особенности работы с Comparator
Не нарушайте контракт
- Если compare(a, b) возвращает 0, то compare(b, a) тоже должен вернуть 0.
- Если compare(a, b) > 0, то compare(b, a) < 0.
- Учитывайте возможные null-значения (см. ниже).
Не забывайте про equals и hashCode
Хотя компараторы и сравнивают объекты «по-своему», для структур наподобие TreeSet или при поиске ключей в TreeMap важно, чтобы логика сравнения по компаратору была согласована с equals. Иначе можно получить неожиданные результаты: два разных объекта считаются равными по компаратору, но не равны по equals.
Сортировка с учётом null
Если поля могут быть null, используйте «готовые» помощники:
Comparator<Person> byLastNameNullSafe = Comparator.comparing(
Person::getLastName,
Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);
7. Полезные нюансы
Таблица: Сравнение Comparable и Comparator
| Comparable | Comparator | |
|---|---|---|
| Где реализуется? | В самом классе | В отдельном классе/лямбде |
| Метод | |
|
| Сколько вариантов? | Только один «естественный» | Сколько угодно, под любые нужды |
| Применение | |
|
| Можно для чужих классов? | Нет | Да |
Пример: сортировка по убыванию
Инвертировать порядок можно вручную:
Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);
Или с помощью reversed():
Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());
8. Типичные ошибки при работе с Comparator
Ошибка №1: Нарушение контракта сравнения. Если вы забыли, что compare(a, b) и compare(b, a) должны быть противоположны по знаку, или возвращаете произвольные значения (например, просто разницу — a.getAge() - b.getAge(), что может переполниться), то результат будет непредсказуемым. Используйте Integer.compare, а не вычитание — так безопаснее.
Ошибка №2: Игнорирование null-значений. Если поля, по которым сравниваете, могут быть null, обязательно обработайте этот случай (например, через Comparator.nullsFirst/Comparator.nullsLast), иначе легко получить NullPointerException в самый неожиданный момент.
Ошибка №3: Нестабильные критерии сортировки. Если компаратор возвращает разные значения для одних и тех же объектов (например, использует случайное число или сильно изменяемое поле), сортировка может вести себя хаотично.
Ошибка №4: Несогласованность с equals. Если compare(a, b) == 0, но a.equals(b) равно false, коллекции вроде TreeSet и TreeMap могут работать не так, как вы ожидаете. Желательно, чтобы равенство по компаратору и по equals совпадали.
Ошибка №5: Сортировка без компаратора для чужих классов. Если вы пытаетесь сортировать объекты «чужого» класса без Comparable и без передачи Comparator, получите ошибку компиляции. Передавайте явный компаратор.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ