JavaRush /Курси /JAVA 25 SELF /Інтерфейс Comparator: створення та використання

Інтерфейс Comparator: створення та використання

JAVA 25 SELF
Рівень 29 , Лекція 3
Відкрита

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
Де реалізується? У самому класі В окремому класі/лямбді
Метод
int compareTo(T o)
int compare(T o1, T o2)
Скільки варіантів? Лише один «природний» Скільки завгодно, під будь-які потреби
Застосування
Collections.sort(list)
Collections.sort(list, comp)
Можна для чужих класів? Ні Так

Приклад: сортування за спаданням

Інвертувати порядок можна вручну:

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, отримаєте помилку компіляції. Передавайте явний компаратор.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ