Питання 29 · Розділ 4

Що таке Comparable та Comparator?

Collections.sort(list) — приймає List і опціонально Comparator. list.sort(comparator) — метод за замовчуванням з Java 8. Для ArrayList обидва однакові. Віддавайте перевагу list....

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Comparable — об’єкт САМ знає, як порівнювати себе. Comparator — зовнішній «суддя» порівнює два об’єкти.

// Comparable: всередині класу
class User implements Comparable<User> {
    String name;
    public int compareTo(User other) {
        return this.name.compareTo(other.name);  // За ім'ям
    }
}

// Comparator: окремий «суддя»
Comparator<User> byAge = (u1, u2) -> u1.age - u2.age;

// Використання:
Collections.sort(users);       // Використовує compareTo
users.sort(byAge);             // Використовує Comparator

Collections.sort(list) — приймає List і опціонально Comparator. list.sort(comparator) — метод за замовчуванням з Java 8. Для ArrayList обидва однакові. Віддавайте перевагу list.sort() — коротше.


🟡 Middle Level

Comparable — natural ordering

// String, Integer, LocalDate реалізують Comparable
"apple".compareTo("banana");  // → від'ємне
LocalDate.now().compareTo(LocalDate.MAX);  // → від'ємне

// Тільки ОДИН порядок на клас

Comparator — безліч стратегій

// Java 8+: декларативний стиль
Comparator<User> comparator = Comparator
    .comparing(User::getLastName)           // За прізвищем
    .thenComparing(User::getFirstName)      // Потім за ім'ям
    .thenComparingInt(User::getAge)         // Потім за віком
    .reversed();                            // У зворотному порядку

// Обробка null
Comparator<User> safe = Comparator
    .comparing(User::getName, Comparator.nullsLast(String::compareTo));

Небезпека віднімання

// ❌ Переповнення!
return o1.id - o2.id;  // Може вийти за межі int!

// ✅ Правильно:
return Integer.compare(o1.id, o2.id);
return Long.compare(o1.id, o2.id);

Наприклад: o1.id = -2_000_000_000, o2.id = 2_000_000_000. Результат: -2_000_000_000 - 2_000_000_000 = -4_000_000_000 — за межами int (max 2_147_483_647). Вийде додатне число, сортування неправильне.


🔴 Senior Level

Коли НЕ використовувати Comparable

  1. Потрібно кілька варіантів сортування одного класу — використовуйте Comparator
  2. Клас із сторонньої бібліотеки (не можна змінити) — використовуйте Comparator
  3. Порядок залежить від контексту/локалі (сортування рядків для різних мов) — Comparator

Узгодженість з equals

// TreeSet/TreeMap використовують compareTo, НЕ equals!
// Якщо compareTo = 0 → елемент вже є
// Навіть якщо equals = false!

// РЕКОМЕНДУЄТЬСЯ:
// (x.compareTo(y) == 0) == (x.equals(y))

// Якщо не узгоджені → втрата даних у TreeSet!

compareTo vs compare

// Comparable:
this.compareTo(other)  // «я» порівнюю з «іншим»

// Comparator:
compare(o1, o2)  // «суддя» порівнює двох

Timsort

// Collections.sort() = Timsort
// → Гібрид Merge Sort + Insertion Sort
// → Stable (рівні зберігають порядок)
// → O(n log n)
// → Вимагає транзитивності компаратора!

// Порушення транзитивності:
// A > B, B > C, але A < C
// → IllegalArgumentException!

Timsort — алгоритм сортування Java. Стабільний (рівні елементи зберігають початковий порядок). Транзитивність — якщо A > B і B > C, то обов’язково A > C. Порушення призводить до IllegalArgumentException.

Production Experience

Реальний сценарій: переповнення

// Comparator для long ID
return (int)(o1.id - o2.id);  // Каст в int!
// → При великих ID → неправильний знак
// → Неправильне сортування

// Рішення: Long.compare(o1.id, o2.id)

Best Practices

  1. Comparable = natural ordering (один на клас)
  2. Comparator = множинні стратегії
  3. Comparator.comparing → читабельно, безпечно
  4. Integer/Long.compare → захист від переповнення
  5. nullsFirst/nullsLast → обробка null
  6. compareTo = equals для TreeSet/TreeMap
  7. Транзитивність обов’язкова!

Резюме для Senior

  • Comparable = внутрішній, natural ordering
  • Comparator = зовнішній, множинні стратегії
  • comparing/thenComparing → декларативний API
  • Переповнення → Integer/Long.compare
  • compareTo vs equals → критично для TreeSet
  • Timsort → stable, вимагає транзитивності

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Comparable — об’єкт порівнює себе сам (compareTo), один порядок на клас (natural ordering)
  • Comparator — зовнішній «суддя» (compare), множинні стратегії сортування
  • Comparator.comparing().thenComparing().reversed() — декларативний API Java 8+
  • Віднімання o1.id - o2.id небезпечне — переповнення int; використовувати Integer.compare()/Long.compare()
  • TreeSet/TreeMap використовують compareTo, НЕ equals — неузгодженість = втрата даних
  • Timsort — стабільне сортування O(n log n), вимагає транзитивності компаратора
  • Comparator.nullsFirst()/nullsLast() — безпечна обробка null

Часті уточнюючі запитання:

  • Чому o1.id - o2.id небезпечний? — При великих значеннях переповнення int: -2 млрд - 2 млрд = -4 млрд (вихід за межі), результат — неправильний знак.
  • Що якщо compareTo і equals не узгоджені в TreeSet?compareTo = 0 означає «елемент вже є», навіть якщо `equals = false» — дані втрачаються.
  • Що таке транзитивність компаратора? — Якщо A > B і B > C, то обов’язково A > C. Порушення → IllegalArgumentException від Timsort.
  • Коли НЕ використовувати Comparable? — Кілька варіантів сортування, сторонній клас (не можна змінити), контекстно-залежний порядок (локаль).

Червоні прапорці (НЕ говорити):

  • «Comparator задає природний порядок класу» — ні, це робить Comparable
  • «Віднімання — безпечний спосіб порівняння чисел» — ні, переповнення int ламає сортування
  • «TreeSet використовує equals для порівняння» — ні, TreeSet використовує compareTo
  • «Можна порушити транзитивність компаратора» — ні, Timsort викине IllegalArgumentException

Пов’язані теми:

  • [[25. В чому різниця між Iterator та ListIterator]]
  • [[31. Які операції підтримує інтерфейс Collection]]