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. Найкращі практики та особливості роботи з 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, отримаєте помилку компіляції. Передавайте явний компаратор.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ