JavaJuniorTechnical

В чём разница между Comparable и Comparator?

Comparable определяет естественный порядок внутри класса (compareTo), Comparator — внешний порядок, передаваемый в коллекции и сортировку. Comparator можно составлять через comparing/thenComparing без изменения класса.

Контракты интерфейсов

// Comparable — естественный порядок, живёт ВНУТРИ класса
public interface Comparable<T> {
    int compareTo(T other);  // <0, 0, >0
}

// Comparator — внешний порядок, НЕЗАВИСИМ от класса
@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

Comparable: реализация внутри класса

public final class Version implements Comparable<Version> {
    private final int major, minor, patch;

    public Version(int major, int minor, int patch) {
        this.major = major;
        this.minor = minor;
        this.patch = patch;
    }

    @Override
    public int compareTo(Version other) {
        // Integer.compare безопаснее, чем вычитание (нет overflow)
        int c = Integer.compare(this.major, other.major);
        if (c != 0) return c;
        c = Integer.compare(this.minor, other.minor);
        if (c != 0) return c;
        return Integer.compare(this.patch, other.patch);
    }

    @Override
    public boolean equals(Object o) { /* … */ }
    @Override
    public int hashCode() { /* … */ }
}

// Работает сразу с TreeSet, Collections.sort, Arrays.sort
TreeSet<Version> versions = new TreeSet<>();
versions.add(new Version(2, 0, 0));
versions.add(new Version(1, 9, 3));
System.out.println(versions.first()); // 1.9.3

Comparator: внешний, составной порядок

record Employee(String name, String department, int salary) {}

List<Employee> staff = List.of(
    new Employee("Alice", "Engineering", 120_000),
    new Employee("Bob",   "Engineering",  95_000),
    new Employee("Carol", "Marketing",   100_000)
);

// Сортировка: сначала по отделу, потом по зарплате убывающей
Comparator<Employee> comp = Comparator
    .comparing(Employee::department)
    .thenComparing(Comparator.comparingInt(Employee::salary).reversed());

List<Employee> sorted = staff.stream()
    .sorted(comp)
    .toList();
// Engineering: Alice(120k), Bob(95k)  →  Marketing: Carol(100k)

Nulls и локализация

// Работа с null-значениями
Comparator<String> nullSafe = Comparator.nullsLast(
    Comparator.naturalOrder()
);

// Локализованная сортировка строк
Collator ruCollator = Collator.getInstance(Locale.forLanguageTag("ru"));
Comparator<String> ruComp = ruCollator::compare;

List<String> names = new ArrayList<>(List.of("Яков", "Андрей", "Борис"));
names.sort(ruComp);
System.out.println(names); // [Андрей, Борис, Яков]

Контракт: согласованность с equals

// ВАЖНО: если a.compareTo(b) == 0, желательно a.equals(b) == true
// TreeMap и TreeSet используют compareTo/compare для проверки равенства,
// а не equals!
TreeMap<Version, String> map = new TreeMap<>();
map.put(new Version(1, 0, 0), "stable");
// map.get(new Version(1, 0, 0)) работает через compareTo, не equals

Когда что использовать

  • Comparable — есть единственный очевидный «естественный» порядок (числа, даты, версии, строки).
  • Comparator — нужно несколько порядков, класс чужой (String, LocalDate), или порядок зависит от контекста (локаль, роль пользователя).

Подводные камни

  • Вычитание вместо Integer.compare. return a - b даёт integer overflow при a=Integer.MAX_VALUE, b=-1; всегда используйте Integer.compare(a, b).
  • TreeMap/TreeSet игнорируют equals. Два объекта с compareTo==0 считаются одним ключом, даже если equals возвращает false — потеря данных без предупреждения.
  • Нарушение транзитивности. Если comp.compare(a,b)>0 и comp.compare(b,c)>0, то comp.compare(a,c) обязан быть >0; нарушение контракта делает поведение сортировки неопределённым (JDK бросает IllegalArgumentException в TimSort начиная с Java 7).
  • Comparator.reversed() и null. naturalOrder().reversed() не обрабатывает null; оборачивайте в nullsFirst/nullsLast до reversed().
  • Comparator как лямбда не сериализуем. Если Comparator передаётся в Serializable-коллекцию (например, TreeMap в сессии), лямбда выбросит NotSerializableException.
  • comparing() использует equals для boxing. Comparator.comparingInt лучше чем comparing(e -> e.salary()) для примитивов — избегает автобоксинга при каждом сравнении.
  • Изменяемое поле в Comparable. Если поле, по которому сравниваете, изменяется после вставки в TreeSet/TreeMap, объект «теряется» в структуре — не найти ни get, ни contains.
  • Несовместимость Comparable с subclassing. Добавление поля в подкласс и расширение compareTo нарушает симметрию с суперклассом; предпочитайте composition over inheritance для value objects.

Common mistakes

  • Путать термин «comparable comparator» с соседним механизмом Java.
  • Не называть границу lifecycle, transaction, thread или request для «comparable comparator».
  • Игнорировать production-эффекты «comparable comparator»: latency, SQL shape, memory, security или observability.

What the interviewer is testing

  • Попросить объяснить механизм «comparable comparator» на минимальном примере.
  • Проверить, видит ли кандидат failure mode и диагностику для «comparable comparator».
  • Уточнить, какие настройки или API меняют «comparable comparator» в реальном сервисе.

Sources

Related topics