W3docs

Java Comparable и Comparator

Определяйте естественный порядок через Comparable и внешний через Comparator в Java, а также составляйте компараторы.

Два интерфейса, одна задача: сообщить Java, когда один объект «меньше» другого. На месте вызова они выглядят почти одинаково, а их методы возвращают одно и то же — отрицательный int, ноль или положительный int. Разница в том, где хранится порядок:

  • Comparable<T> — тип сам знает, как упорядочивать свои экземпляры. Его метод int compareTo(T other) задаёт естественный порядок типа.
  • Comparator<T> — внешний объект, упорядочивающий экземпляры. Его метод int compare(T a, T b) описывает один из множества возможных порядков.

Comparable реализуют тогда, когда для типа есть один очевидный «меньше» — Integer, String, LocalDate. Comparator пишут для всех остальных порядков — по длине, по имени без учёта регистра, по убыванию цены, по любому критерию, который можно выразить кодом. У большинства типов один Comparable (или ни одного) и множество полезных Comparator-ов.

Контракт: −/0/+

Оба метода возвращают int, знак которого является ответом:

  • отрицательныйa стоит перед b
  • ноль — равны с точки зрения порядка
  • положительныйa стоит после b

Конкретная величина не важна. -1 и -1_000_000 означают одно и то же. Никогда не пишите return a.size - b.size, если возможно переполнение: вычитание Integer.MIN_VALUE из положительного числа приводит к переполнению. Вместо этого используйте Integer.compare(a.size(), b.size()) — он безопасен в отношении переполнения и содержит столько же символов.

Comparable<T> — естественный порядок

Тип реализует Comparable<Self> и предоставляет compareTo:

public record Version(int major, int minor, int patch) implements Comparable<Version> {
  @Override public int compareTo(Version other) {
    int m = Integer.compare(this.major, other.major);
    if (m != 0) return m;
    int n = Integer.compare(this.minor, other.minor);
    if (n != 0) return n;
    return Integer.compare(this.patch, other.patch);
  }
}

Теперь Collections.sort(versions), versions.stream().sorted(), new TreeSet<Version>() и new TreeMap<Version, X>() работают без передачи каких-либо дополнительных аргументов.

Контракт предусматривает три правила, которые должен соблюдать каждый compareTo:

  1. Антисимметричностьa.compareTo(b) и b.compareTo(a) имеют противоположные знаки.
  2. Транзитивность — если a < b и b < c, то a < c.
  3. Согласованность с equals (настоятельно рекомендуется)a.compareTo(b) == 0 тогда и только тогда, когда a.equals(b).

Третье правило нарушают чаще всего. BigDecimal — классический пример: new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) равно 0, но .equals возвращает false. В результате TreeSet<BigDecimal> и HashSet<BigDecimal> будут расходиться во мнениях о том, являются ли "1.0" и "1.00" дубликатами. По возможности придерживайтесь согласованности.

Comparator<T> — внешний порядок

Comparator — это отдельный объект. Он может сравнивать любые два T, в том числе типы, которые вы не писали:

Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
list.sort(byLength);

Поскольку Comparator<T> является функциональным интерфейсом (один абстрактный метод compare), каждый Comparator — это просто лямбда или ссылка на метод. Именно так выглядит современный код с компараторами — анонимные классы почти не используются.

Строители на Comparator

Класс содержит статические фабричные методы, делающие построение компараторов кратким и читаемым:

Comparator<Person> byAge       = Comparator.comparingInt(Person::age);
Comparator<Person> byName      = Comparator.comparing(Person::name);
Comparator<Person> byNameCi    = Comparator.comparing(Person::name, String.CASE_INSENSITIVE_ORDER);
Comparator<Person> oldestFirst = byAge.reversed();
Comparator<String> nullsFirst  = Comparator.nullsFirst(Comparator.naturalOrder());

Используйте специализированные примитивные строители — comparingInt, comparingLong, comparingDouble — когда ключ является примитивом. Они позволяют избежать автоупаковки при каждом сравнении, что ощутимо сказывается при длинной сортировке.

Цепочки компараторов с thenComparing

Ещё одна причина отдавать предпочтение строителям: можно объединять несколько ключей в цепочку.

Comparator<Person> ordering =
    Comparator.comparing(Person::lastName)
              .thenComparing(Person::firstName)
              .thenComparingInt(Person::age);

Читается сверху вниз: «первичный ключ — фамилия; при равенстве — имя; затем — возраст». thenComparing вызывается на предыдущем компараторе и возвращает новый, который обращается ко второму ключу только при равенстве по первому. Длина цепочки не ограничена.

reversed(), nullsFirst, nullsLast

Три модификатора используются постоянно:

  • reversed() переворачивает порядок любого компаратора. byAge.reversed() — «сначала старшие».
  • nullsFirst(cmp) оборачивает компаратор так, чтобы null-значения считались меньше любого ненулевого. Полезно при сортировке коллекций, которые могут содержать null.
  • nullsLast(cmp) — симметричный аналог.

Не вызывайте reversed() на составном компараторе в ожидании, что перевернётся только последний ключ — reversed() переворачивает весь порядок, каждый ключ в цепочке.

Comparable и Comparator в API JDK

Многие методы существуют в двух вариантах — один использует естественный порядок, другой принимает Comparator:

ОперацияПерегрузка с естественным порядкомПерегрузка с Comparator
Сортировка спискаCollections.sort(list)Collections.sort(list, cmp)
Сортировка списка (современный способ)list.sort(null)list.sort(cmp)
Сортировка потокаstream.sorted()stream.sorted(cmp)
Множество на основе дереваnew TreeSet<>()new TreeSet<>(cmp)
Карта на основе дереваnew TreeMap<>()new TreeMap<>(cmp)
Минимум/максимумCollections.min(list)Collections.min(list, cmp)
Бинарный поискCollections.binarySearch(list, key)Collections.binarySearch(list, key, cmp)
PriorityQueueестественный порядок типа элементаконструктор принимает Comparator

Перегрузки с естественным порядком требуют, чтобы тип элемента реализовывал Comparable. Если ваш тип его не реализует, при вызове этих методов вы получите ClassCastException во время выполнения — а не ошибку компиляции, — поскольку приведение типа происходит внутри реализации сортировки.

Практический пример: естественный порядок, пользовательские компараторы, цепочки ключей, null

Программа ниже определяет запись с естественным порядком (Comparable) и тремя внешними порядками: по одному ключу, по цепочке ключей с инвертированным вторичным ключом, и один, допускающий записи null.

java— editable, runs on the server

Что следует вынести из выполнения программы:

  • Реализация Comparable выполнила сортировку по имени и разрешала совпадения имён по возрасту. Явный компаратор не потребовался — естественный порядок используется Collections.sort и другими подобными методами по умолчанию.
  • Comparator.comparingDouble(Person::salary) короче и быстрее, чем (a, b) -> Double.compare(a.salary(), b.salary()), поскольку избегает автоупаковки.
  • Составной компаратор отсортировал прежде всего по возрасту, а reversed() применил только к части salary — это правильный паттерн, когда нужны разные направления для разных ключей. Сравните с вызовом .reversed() на всей цепочке, который перевернул бы оба ключа.
  • nullsFirst позволил компаратору обработать список, содержащий записи null, без NullPointerException. Без этой обёртки первое сравнение с участием null завершилось бы ошибкой.
  • «Трюк с вычитанием» дал неверный результат для Integer.MAX_VALUE - (-1): это вычисление переполняется до отрицательного числа, поэтому bad сообщает, что MAX_VALUE меньше -1. Integer.compare всегда возвращает правильный знак. Всегда предпочитайте его.

Что дальше

Теперь вы разобрались с итерацией (Iterator / ListIterator) и упорядочением (Comparable / Comparator). Следующая глава объединяет всё это в служебном классе java.util.Collections — статическом наборе методов sort, search, reverse, shuffle, min, max и «обернуть эту коллекцию как неизменяемую», работающих с любым List, Set или Map. После этого две короткие главы посвящены сортировке и поиску.

Практика

Практика
Вы пишете `list.sort((a, b) -> a.scoreDifference(b))`, где `scoreDifference` возвращает `a.score - b.score` как `int`. Список содержит оценки включая `Integer.MAX_VALUE` и `Integer.MIN_VALUE`, и результат явно неверен. Как это исправить?
Вы пишете `list.sort((a, b) -> a.scoreDifference(b))`, где `scoreDifference` возвращает `a.score - b.score` как `int`. Список содержит оценки включая `Integer.MAX_VALUE` и `Integer.MIN_VALUE`, и результат явно неверен. Как это исправить?
Was this page helpful?