Що таке Comparable та Comparator?
Collections.sort(list) — приймає List і опціонально Comparator. list.sort(comparator) — метод за замовчуванням з Java 8. Для ArrayList обидва однакові. Віддавайте перевагу list....
🟢 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
- Потрібно кілька варіантів сортування одного класу — використовуйте Comparator
- Клас із сторонньої бібліотеки (не можна змінити) — використовуйте Comparator
- Порядок залежить від контексту/локалі (сортування рядків для різних мов) — 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
- Comparable = natural ordering (один на клас)
- Comparator = множинні стратегії
- Comparator.comparing → читабельно, безпечно
- Integer/Long.compare → захист від переповнення
- nullsFirst/nullsLast → обробка null
- compareTo = equals для TreeSet/TreeMap
- Транзитивність обов’язкова!
Резюме для 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]]