Вопрос 29 · Раздел 4

Что такое Comparable и Comparator?

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

Версии по языкам: 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]]