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» в реальном сервисе.