Коллекции ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-10-23

Интерфейсы Comparable и Comparator в Java

В TreeSet добавили четырёх человек, а вывели троих: «Вася Иванов» пропал без исключения и предупреждения. Компаратор сравнивал только фамилии, вернул 0 для двух Ивановых — и для TreeSet это означало «такой элемент уже есть». Чтобы такие ошибки не случались, нужно понимать, как работают Comparable и Comparator.

Comparable и Comparator — два интерфейса Java для сравнения объектов. Comparable<T> (пакет java.lang) задаёт естественный порядок: класс сам реализует метод compareTo(T o). Comparator<T> (пакет java.util), или компаратор, — внешнее правило сравнения: метод compare(T o1, T o2) реализуют в отдельном классе или лямбдой, и таких правил для одного класса может быть сколько угодно. Оба метода возвращают отрицательное число, ноль или положительное число.

Интерфейс Comparable и метод compareTo

Интерфейс Comparable<T> реализуют классы, у объектов которых есть естественный порядок: числа сравниваются по величине, строки — по алфавиту, даты — по времени. У интерфейса единственный метод int compareTo(T o): он сравнивает текущий объект (this) с переданным. Его уже реализуют String, Integer, LocalDate, BigDecimal и все enum.

Результат compareTo() читается так:

  • отрицательное число — текущий объект должен стоять перед переданным;
  • положительное число — после переданного;
  • ноль — объекты равны с точки зрения сортировки (это не обязательно значит, что equals() вернёт true, см. раздел Контракт compareTo).

Параметр типа в Comparable<Person> важен: метод получает compareTo(Person other), и приведение типа не нужно. С «сырым» Comparable сигнатура была бы compareTo(Object o), а ошибка типа обнаружилась бы только при выполнении.

Пример реализации интерфейса Comparable в Java — класс Person, естественный порядок которого задан по возрасту:

import java.util.Objects;

public class Person implements Comparable<Person> {
    private final String firstName;
    private final String lastName;
    private final int age;

    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public int getAge() {
        return age;
    }

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age
                && Objects.equals(firstName, person.firstName)
                && Objects.equals(lastName, person.lastName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName, age);
    }

    @Override
    public String toString() {
        return firstName + " " + lastName + ", " + age;
    }
}

TreeSet, созданный без компаратора, расставляет элементы по compareTo():

import java.util.SortedSet;
import java.util.TreeSet;

public class ComparePersonExample {
    public static void main(String[] args) {
        SortedSet<Person> set = new TreeSet<>();
        set.add(new Person("Саша", "Иванов", 36));
        set.add(new Person("Маша", "Петрова", 23));
        set.add(new Person("Даша", "Сидорова", 34));
        set.add(new Person("Вася", "Иванов", 25));
        set.forEach(System.out::println);
    }
}

Вывод:

Маша Петрова, 23
Вася Иванов, 25
Даша Сидорова, 34
Саша Иванов, 36

Если класс не реализует Comparable, а компаратор не передан, TreeSet упадёт уже на первом add() с ClassCastException: class Cat cannot be cast to class java.lang.Comparable. Ошибку не поймает компилятор: конструктор new TreeSet<>() принимает любой тип элементов. Сами методы TreeSet (first(), headSet(), subSet()) разобраны в уроке SortedSet и TreeSet.

Как compareTo сравнивает строки

String.compareTo() сравнивает строки посимвольно по кодам UTF-16 и возвращает разность первых несовпавших символов. Поэтому все заглавные буквы идут раньше строчных, а буква «Ё» (U+0401) — даже раньше «А» (U+0410). Что выведет этот код?

import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class StringOrderExample {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>(List.of("яблоко", "Ёлка", "арбуз", "Банан"));

        words.sort(null);                              // естественный порядок
        System.out.println(words);

        words.sort(String.CASE_INSENSITIVE_ORDER);     // без учёта регистра
        System.out.println(words);

        words.sort(Collator.getInstance(Locale.forLanguageTag("ru")));  // по правилам языка
        System.out.println(words);
    }
}

Вывод:

[Ёлка, Банан, арбуз, яблоко]
[арбуз, Банан, яблоко, Ёлка]
[арбуз, Банан, Ёлка, яблоко]

Даже CASE_INSENSITIVE_ORDER ставит «Ёлку» в конец: строчная «ё» (U+0451) идёт в таблице после «я» (U+044F). Алфавитный порядок, как в словаре, даёт только Collator — он реализует Comparator и учитывает правила языка.

Интерфейс Comparator и метод compare

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

Классический способ — отдельный класс, который реализует компаратор:

import java.util.Comparator;

public class PersonComparator implements Comparator<Person> {
    @Override
    public int compare(Person o1, Person o2) {
        return o1.getLastName().compareTo(o2.getLastName());
    }
}

Компаратор передают в конструктор TreeSet, и он заменяет естественный порядок по возрасту:

import java.util.SortedSet;
import java.util.TreeSet;

public class PersonComparatorExample {
    public static void main(String[] args) {
        SortedSet<Person> set = new TreeSet<>(new PersonComparator());
        set.add(new Person("Саша", "Иванов", 36));
        set.add(new Person("Маша", "Петрова", 23));
        set.add(new Person("Даша", "Сидорова", 34));

        boolean added = set.add(new Person("Вася", "Иванов", 25));
        System.out.println("Вася добавлен: " + added + ", размер: " + set.size());
        set.forEach(System.out::println);
    }
}

Вывод:

Вася добавлен: false, размер: 3
Саша Иванов, 36
Маша Петрова, 23
Даша Сидорова, 34

Куда пропал второй Иванов

TreeSet и ключи TreeMap определяют дубликаты не через equals(), а через компаратор: если compare() вернул 0, новый элемент считается уже существующим и не добавляется. «Вася Иванов» и «Саша Иванов» по equals() разные, но компаратор смотрит только на фамилию. Чтобы сохранить обоих, добавьте в компаратор второй критерий — thenComparing() из следующего раздела.

Разница между Comparable и Comparator

Главная разница: Comparable описывает один естественный порядок внутри самого класса, а Comparator — любое количество внешних правил сортировки, для которых исходный класс менять не нужно. Если порядок очевиден и единственный (числа, даты, коды), реализуют Comparable; для всех остальных случаев пишут компаратор.

Признак Comparable Comparator
Пакет java.lang java.util
Метод int compareTo(T o) — сравнивает this с o int compare(T o1, T o2) — сравнивает два переданных объекта
Где реализуется В самом сравниваемом классе В отдельном классе, анонимном классе или лямбде
Сколько порядков Один — естественный Сколько угодно
Нужен доступ к коду класса Да Нет — подходит для String и чужих классов
Как подключается Collections.sort(list), list.sort(null), new TreeSet<>() list.sort(cmp), Arrays.sort(arr, cmp), new TreeSet<>(cmp)
Готовые методы Нет, только compareTo() comparing(), thenComparing(), reversed(), nullsFirst() и другие (Java 8)
Аннотация @FunctionalInterface Нет Да

Подходы не исключают друг друга: класс Person из примеров реализует Comparable по возрасту, и при этом его сортируют компаратором по фамилии. Переданный компаратор всегда имеет приоритет над естественным порядком.

Comparator.comparing, thenComparing, reversed

С Java 8 компаратор почти никогда не пишут отдельным классом: его собирают из статических и default-методов интерфейса Comparator. Comparator.comparing(Person::getLastName) сравнивает по фамилии, thenComparing() добавляет следующий критерий для равных значений, reversed() переворачивает порядок.

Метод Что делает Нюанс
static comparing(Function keyExtractor) Сравнивает по ключу, который возвращает функция (ключ должен быть Comparable) Ключ null — NullPointerException при сравнении
static comparing(Function keyExtractor, Comparator keyCmp) Сравнивает ключи указанным компаратором Например, comparing(Person::getLastName, Comparator.reverseOrder())
static comparingInt / comparingLong / comparingDouble Сравнивает по примитивному ключу Без автоупаковки в Integer; внутри — Integer.compare(), переполнения нет
default thenComparing(...), thenComparingInt(...) Следующий критерий, если предыдущий вернул 0 Можно вызывать цепочкой сколько угодно раз
default reversed() Обратный порядок Переворачивает всю цепочку, к которой применён, а не только последний критерий
static naturalOrder() / reverseOrder() Естественный порядок Comparable / обратный ему Comparator.reverseOrder() делает то же, что Collections.reverseOrder()
static nullsFirst(cmp) / nullsLast(cmp) Ставит null в начало / в конец, остальные сравнивает через cmp Без них null в сортируемом списке — NullPointerException

Пример: сортировка списка по фамилии, при равных фамилиях — по возрасту, а затем по возрасту в обратном порядке:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class PersonComparingExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(List.of(
                new Person("Саша", "Иванов", 36),
                new Person("Маша", "Петрова", 23),
                new Person("Даша", "Сидорова", 34),
                new Person("Вася", "Иванов", 25)));

        people.sort(Comparator.comparing(Person::getLastName)
                .thenComparingInt(Person::getAge));
        people.forEach(System.out::println);

        System.out.println("---");
        people.sort(Comparator.comparingInt(Person::getAge).reversed());
        people.forEach(System.out::println);
    }
}

Вывод:

Вася Иванов, 25
Саша Иванов, 36
Маша Петрова, 23
Даша Сидорова, 34
---
Саша Иванов, 36
Даша Сидорова, 34
Вася Иванов, 25
Маша Петрова, 23

Теперь оба Ивановых на месте: при равных фамилиях работает второй критерий. Тот же компаратор, переданный в new TreeSet<>(...), тоже сохранит обоих.

Сортировка списка с null

Естественный порядок не умеет сравнивать null: "Анна".compareTo(null) бросает NullPointerException. Обёртка nullsFirst() или nullsLast() решает это без проверок в каждом сравнении:

List<String> names = new ArrayList<>(Arrays.asList("Олег", null, "Анна"));

names.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(names);   // Вывод: [null, Анна, Олег]

names.sort(Comparator.naturalOrder());   // NullPointerException

Компаратор через лямбду и ссылку на метод

Comparator — функциональный интерфейс, поэтому вместо класса PersonComparator достаточно лямбда-выражения (p1, p2) -> p1.getLastName().compareTo(p2.getLastName()) или ещё короче — Comparator.comparing(Person::getLastName). Все три записи задают один и тот же порядок.

Comparator<Person> byClass  = new PersonComparator();
Comparator<Person> byLambda = (p1, p2) -> p1.getLastName().compareTo(p2.getLastName());
Comparator<Person> byRef    = Comparator.comparing(Person::getLastName);

На чём чаще всего спотыкаются — на лямбде внутри цепочки. Этот код не скомпилируется:

people.sort(Comparator.comparing(p -> p.getLastName()).reversed());
// error: cannot find symbol
//   symbol:   method getLastName()
//   location: variable p of type Object

Компилятор выводит тип p из выражения comparing(...), а у него нет целевого типа: следом вызывается .reversed(), и информация из people.sort() до лямбды не доходит. Тип p становится Object. Исправления: ссылка на метод Comparator.comparing(Person::getLastName).reversed(), явный тип параметра comparing((Person p) -> p.getLastName()) или явный тип-аргумент Comparator.<Person, String>comparing(p -> p.getLastName()). Без .reversed() лямбда comparing(p -> p.getLastName()) компилируется нормально.

Контракт compareTo: переполнение и согласованность с equals

Методы compareTo() и compare() должны соблюдать контракт: знак a.compareTo(b) противоположен знаку b.compareTo(a), порядок транзитивен (если a > b и b > c, то a > c), а равные элементы одинаково сравниваются с третьими. Если контракт нарушен, сортировка даёт неверный результат или падает с IllegalArgumentException: Comparison method violates its general contract!.

Почему нельзя писать return a - b

Вычитание — самый частый способ нарушить контракт. Для небольших чисел a - b даёт правильный знак, но при переполнении int знак меняется на противоположный:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class OverflowExample {
    public static void main(String[] args) {
        Comparator<Integer> bad = (a, b) -> a - b;
        Comparator<Integer> good = Integer::compare;

        System.out.println(bad.compare(Integer.MIN_VALUE, 1));
        System.out.println(good.compare(Integer.MIN_VALUE, 1));

        List<Integer> nums = new ArrayList<>(List.of(1, Integer.MIN_VALUE));
        nums.sort(bad);
        System.out.println(nums);
        nums.sort(good);
        System.out.println(nums);
    }
}

Вывод:

2147483647
-1
[1, -2147483648]
[-2147483648, 1]

Integer.MIN_VALUE - 1 переполняется и превращается в Integer.MAX_VALUE, поэтому «плохой» компаратор считает минус два миллиарда больше единицы, и список остаётся неотсортированным.

Сравнивайте числа без вычитания

Вместо return this.age - other.age пишите Integer.compare(this.age, other.age), для long и double — Long.compare() и Double.compare(), а в цепочках — Comparator.comparingInt(). Возраст до переполнения не дорастёт, но привычка к вычитанию однажды сработает на идентификаторах, суммах в копейках или отрицательных значениях, и такую ошибку тесты на маленьких числах не поймают.

Согласованность compareTo с equals

Порядок называют согласованным с equals, если a.compareTo(b) == 0 тогда и только тогда, когда a.equals(b). Документация Java настоятельно рекомендует, но не требует этого. Нарушение не ломает сортировку списка, но меняет поведение TreeSet и TreeMap: они ищут и отбрасывают дубликаты по compareTo()/compare(), а HashSet — по equals() и hashCode(). Классический пример из стандартной библиотеки — BigDecimal:

List<BigDecimal> values = List.of(new BigDecimal("1.0"), new BigDecimal("1.00"));

Set<BigDecimal> hashSet = new HashSet<>(values);
Set<BigDecimal> treeSet = new TreeSet<>(values);

System.out.println(hashSet.size());   // Вывод: 2 — equals() учитывает масштаб
System.out.println(treeSet.size());   // Вывод: 1 — compareTo() вернул 0
System.out.println(treeSet);          // Вывод: [1.0]

Наш Person тоже не согласован с equals(): два разных человека одного возраста для compareTo() равны, и второго из них TreeSet без компаратора не примет — как в истории с Ивановыми. Если объекты будут храниться в TreeSet или быть ключами TreeMap, включайте в сравнение все поля, которые участвуют в equals(). Как equals() и hashCode() работают в хеш-коллекциях, объясняет урок Set и HashSet.

Где используются Comparable и Comparator

Comparable и Comparator используют все отсортированные коллекции и методы сортировки Java: Collections.sort(), List.sort(), Arrays.sort(), TreeSet, TreeMap, PriorityQueue, Stream.sorted(), Collections.max(). Везде действует одно правило: если компаратор передан, используется он, если нет (или передан null) — естественный порядок Comparable.

Где Без компаратора С компаратором Подробнее
Сортировка списка Collections.sort(list), list.sort(null) list.sort(cmp), Collections.sort(list, cmp) Collections.sort(), List и ArrayList
Сортировка массива Arrays.sort(arr) Arrays.sort(arr, cmp) Компаратор принимают только массивы объектов: для int[] такой перегрузки нет, нужен Integer[]
TreeSet new TreeSet<>() new TreeSet<>(cmp) SortedSet и TreeSet
TreeMap (порядок ключей) new TreeMap<>() new TreeMap<>(cmp) Map
PriorityQueue new PriorityQueue<>() — первым выходит наименьший new PriorityQueue<>(cmp) PriorityQueue
Поиск максимума и минимума Collections.max(coll), Collections.min(coll) Collections.max(coll, cmp), stream.max(cmp) Collections.max(), min()
Stream API stream.sorted() stream.sorted(cmp) Исходная коллекция не меняется

Все сортировки объектов в Java стабильны: элементы, для которых компаратор вернул 0, сохраняют исходный взаимный порядок. Поэтому сортировку по нескольким полям можно собрать и цепочкой thenComparing(), и несколькими сортировками подряд — от последнего критерия к первому.

Часто задаваемые вопросы

Обязательно ли compareTo() возвращать -1, 0 или 1?

Нет, важен только знак: String.compareTo(), например, возвращает разность кодов символов ("Б".compareTo("а") даёт минус 31). Поэтому проверка a.compareTo(b) == 1 — ошибка, правильно писать a.compareTo(b) > 0.

Почему Comparator считается функциональным интерфейсом, если в нём есть метод equals()?

Метод boolean equals(Object obj) в Comparator переобъявляет метод класса Object, а такие методы не учитываются при подсчёте абстрактных. Остальные методы (reversed(), thenComparing(), comparing()) — default и static. Абстрактный метод один — compare(), поэтому компаратор можно задать лямбдой.

Как сравниваются элементы enum?

Все перечисления реализуют Comparable, и естественный порядок — порядок объявления констант (по ordinal()). Метод compareTo() у enum объявлен final, переопределить его нельзя; другой порядок задают компаратором, например Comparator.comparing(Enum::name).

Как отсортировать объекты по убыванию?

Для естественного порядка — list.sort(Comparator.reverseOrder()), для компаратора — добавить .reversed(): Comparator.comparingInt(Person::getAge).reversed(). Если нужно перевернуть только один критерий из цепочки, его передают вторым аргументом: thenComparing(Person::getAge, Comparator.reverseOrder()). Пример с выводом — в разделе Comparator.comparing, thenComparing, reversed.

Практика по теме

Закрепите урок на задачах — у каждой есть уровень и оценка времени.

Все задачи модуля →

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.