Автор
Владимир Портянко
Java-разработчик в Playtika

Метод compareTo

Статья из группы Java Developer
участников
compareTo это единственный метод интерфейса Comparable, и отвечает он на один вопрос: какой из двух объектов меньше. Класс, который реализовал Comparable, получает естественный порядок, и дальше его объекты сортирует сам стандартный Arrays.sort(array) или Collections.sort(list) без всяких дополнительных аргументов. У String, Integer и остальных классов-оберток такой порядок уже задан, а для своего класса compareTo пишут руками. Разберем и то, и другое.

Кратко

  • compareTo(T o) возвращает отрицательное число, если текущий объект меньше аргумента, положительное если больше, и ноль если они равны. Важен только знак, конкретное значение нет.
  • Метод объявлен в интерфейсе Comparable<T> и задает естественный порядок класса: тот единственный порядок, который считается для него порядком по умолчанию.
  • Как только класс реализовал Comparable, его массивы и списки сортируются вызовами Arrays.sort(array) и Collections.sort(list) без второго аргумента.
  • Готовые реализации уже есть у String, Boolean и всех классов-оберток примитивов: строки сравниваются по кодам символов, числа по возрастанию.
  • В своем классе метод пишут так: сравнили по первому критерию, при равенстве перешли ко второму, и так далее до последнего.
  • Когда порядков нужно несколько или класс чужой и править его нельзя, вместо Comparable берут Comparator.
Для упорядочивания объектов одного типа, хранящихся в массиве или коллекции, разработчики Java придумали интерфейс Comparable. В нем объявлен всего один метод, compareTo:
public interface Comparable<T> {
    public int compareTo(T o);
}
Чашечные весы: слева большая желтая рыба, справа маленькая зеленая, сверху подпись compareTo(), на основании весов надпись Comparable для класса FishИнтерфейс Comparable параметризирован типом объекта, который он принимает в качестве параметра в метод compareTo. В данном случае мы предупреждаем компилятор, какие типы объектов собираемся сравнивать, и он же нас и остановит: подсунуть в compareTo объект чужого типа он просто не даст, это будет ошибка компиляции. А вот если дженерики обойти, например сложить разнотипные объекты в массив Object[] или в сырой List, проверка съедет на время выполнения. Тогда при сортировке и прилетит ClassCastException: документация Arrays.sort и Collections.sort прямо требует, чтобы все элементы были взаимно сравнимы. Метод compareTo в Java сравнивает вызывающий объект с объектом, переданным в качестве параметра, и возвращает в результате выполнения сравнения целое число:
  • положительное, если вызывающий объект больше объекта, переданного в качестве параметра;
  • отрицательное, если вызывающий объект меньше объекта, переданного в качестве параметра;
  • нуль, если объекты равны.
Написание логики сравнения объектов это забота исключительно разработчика класса, и определяется она желаемыми результатами при упорядочивании. Обратите внимание на формулировку: положительное, отрицательное, ноль. Ни о каких 1 и -1 речи не идет, и рассчитывать на них нельзя. Тот же String по документации возвращает разность кодов символов в первой несовпавшей позиции, так что "b".compareTo("a") даст 1, а "я".compareTo("а") сразу 31. Поэтому проверку всегда пишут как if (a.compareTo(b) < 0), а не как сравнение с конкретным числом.

Зачем нужен метод compareTo в Java?

Программисту на Java очень часто приходиться иметь дело с массивами и списками объектов. При работе с большим количеством данных их зачастую удобно хранить в упорядоченном или отсортированном виде. Во-первых, это ускоряет работу с коллекцией при поиске нужной информации, во-вторых, упорядоченные данные визуально лучше воспринимаются.
Слева цветные круги и квадраты вперемешку, стрелка с подписью sort, справа те же фигуры разложены по цветам в четыре ряда
Одним из самых простых и эффективных способов отсортировать массив объектов является метод sort() класса Arrays, а коллекцию объектов в виде списка сортирует аналогичный метод класса Collections.

Comparable или Comparator: что выбрать

Для сортировки с помощью этих методов разработчики Java предоставили нам свободу в выборе способа задания критериев сортировки: с реализацией интерфейса Comparable в классе объектов, которые мы хотим упорядочить, или с использованием интерфейса Comparator. В первом случае методы сортировки принимают набор объектов в виде массива или списка:
sort(T[]array)//сортировка массива
sort(List<T> list)// сортировка списка
а во втором плюс еще реализацию интерфейса Comparator:
sort(T[]array, Comparator <? super T> comparator)//сортировка массива
sort(List<T> list, Comparator <? super T> comparator)// сортировка списка
Интерфейс Comparable используется, когда мы хотим задать естественный (наиболее логичный с нашей точки зрения) порядок расположения объектов при сортировке. Он также является способом «зашить» алгоритм сравнения объектов этого класса на стадии его проектирования. Разница между двумя интерфейсами короче всего укладывается в таблицу:
Признак Comparable Comparator
Где живет логика сравнения Внутри самого класса, в методе compareTo Снаружи, в отдельном объекте-компараторе
Метод int compareTo(T o), один аргумент int compare(T o1, T o2), два аргумента
Сколько порядков можно задать Один, он же естественный Сколько угодно, по компаратору на каждый порядок
Нужен ли доступ к исходникам класса Да, класс придется менять Нет, работает и с чужими классами
Как вызывается сортировка Arrays.sort(array), Collections.sort(list) Arrays.sort(array, cmp), Collections.sort(list, cmp)
Когда удобнее Порядок очевиден и он один: строки по алфавиту, числа по возрастанию Порядков много или класс не ваш: сортировать пользователей то по имени, то по возрасту

Где compareTo уже реализован: строки и числа

Например, с помощью реализации этого интерфейса, определены критерии естественного упорядочивания в классах-обертках основных примитивных типов: Byte, Character, Long, Integer, Short, Double, Float, Boolean, String. Это также означает, что в этих классах есть реализованный метод compareTo, который при необходимости мы можем использовать в программе. Давайте посмотрим на примере сравнения строк, как реализован этот метод в классе String.
String str1="Аарон";
        String str2="АAPOH";
        String str3="аарон";
        String str4="ААрон";
        String str5="аАрон";
        String str6="Берта";
        String str7="берта";
String[] allStr=new String[]{str1,str2,str3,str4, str5,str6, str7};
        Arrays.sort(allStr);
        for (String s:allStr){
            System.out.println(s);
        }
Если этот код выполнить в методе main, то получим такой результат:
АAPOH
ААрон
Аарон
Берта
аАрон
аарон
берта
Как видно из примера, в классе String метод compareTo упорядочивает строки лексикографически и с учетом регистра. Именно такой порядок сравнения строк определен разработчиками класса String как естественный. Для более простого понимания, что такое лексикографический порядок, достаточно вспомнить, как расположены слова в языковых словарях. Только со словарем сходство не полное, и вывод выше это показывает. Лексикографический порядок в Java идет не по алфавиту, а по кодам символов: сначала все заглавные, потом все строчные, потому что у заглавных коды меньше. Поэтому «Берта» встала раньше «аАрона», хотя по алфавиту должно быть наоборот. И еще одна деталь, которую в примере легко пропустить: строка "АAPOH" набрана сразу в двух алфавитах. Первая буква кириллическая А с кодом 1040, а остальные четыре латинские, у них коды меньше сотни. Именно поэтому она и оказалась в самом начале списка, обогнав даже "ААрон": на второй позиции у нее латинская A с кодом 65 против кириллической с кодом 1040. Для глаза строки выглядят одинаково, для compareTo это совершенно разные символы, и на таких «одинаковых» строках регулярно ломаются и сравнение, и поиск по коллекциям. Если нужен именно алфавитный порядок конкретного языка, документация String отправляет к отдельному классу Collator. При сравнении чисел объекты упорядочиваются в порядке возрастания. Такая логика сравнения заложена в классах Byte, Character, Long, Integer, Short, Double, Float.

Реализуем сравнение в своем классе

Посмотрим на примере как можно встроить возможность сравнения объектов в свой класс. При реализации метода compareTo в Java мы можем задать один или несколько критериев упорядочивания объектов, а также задействовать методы compareTo из классов String и Integer. Например, для объектов класса User мы задаем сортировку по имени, а в случае равенства имен по возрасту. Объекты будут располагаться в естественном порядке (по мере увеличения значения). Класс User:
public class User  implements Comparable <User>{//добавляем возможность сравнивать объекты User

    private String name;
    private Integer age;
    private String email;

    public User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }

    @Override
//реализуем метод compareTo интерфейса Comparable
    public int compareTo(User o) {

//используем метод compareTo из класса String для сравнения имен
        int result = this.name.compareTo(o.name);

//если имена одинаковые -  сравниваем возраст,
//используя метод compareTo из класса Integer

        if (result == 0) {
            result = this.age.compareTo(o.age);
        }
        return result;
    }

    @Override
    public String toString() {
        return "{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                '}';
    }
}
Протестируем работу метода compareTo, реализованного в классе User, c помощью метода sort класса Collections:
public static void main(String[] args) {
    User user = new User("Андрей", 19, "andryha@mail.ru");
    User user2 = new User("Олег", 25, "oleg@mail.ru");
    User user3 = new User("Андрей", 24,"opr@google.com");
    User user4 = new User("Игорь", 16, "igor@mail.ru");
    User user5 = new User("Андрей", 44,"stary@google.com");
    List<User> list = new ArrayList<>();

    list.add(user);
    list.add(user2);
    list.add(user3);
    list.add(user4);
    list.add(user5);

    System.out.println("-------до сортировки--------");
    for (User u : list) {
        System.out.println(u);
    }
    System.out.println("-------после сортировки-----");
    Collections.sort(list);
    for (User u : list) {
        System.out.println(u);
    }
}
Результат работы метода main:
-------до сортировки--------
{name='Андрей',  age=19,  email='andryha@mail.ru'}
{name='Олег',    age=25,  email='oleg@mail.ru'}
{name='Андрей',  age=24,  email='opr@google.com'}
{name='Игорь',   age=16,  email='igor@mail.ru'}
{name='Андрей',  age=44,  email='stary@google.com'}
-------после сортировки-----
{name='Андрей',  age=19,  email='andryha@mail.ru'}
{name='Андрей',  age=24,  email='opr@google.com'}
{name='Андрей',  age=44,  email='stary@google.com'}
{name='Игорь',   age=16,  email='igor@mail.ru'}
{name='Олег',    age=25,  email='oleg@mail.ru'}

Как то же самое пишут сегодня

Статья написана во времена Java 7, а в Java 8 для ровно этого сценария появился готовый инструмент. Сортировку «по имени, а при равенстве имен по возрасту» теперь собирают из компараторов, не трогая логику сравнения внутри класса. Единственное, что понадобится User, это геттеры getName() и getAge():
list.sort(Comparator.comparing(User::getName).thenComparing(User::getAge));
Метод comparing берет функцию, которая достает из объекта ключ сортировки, thenComparing добавляет следующий критерий, и таких критериев можно навесить сколько угодно. Обратите внимание и на сам вызов: метод sort с Java 8 есть прямо у List, так что Collections.sort стал необязательным. Его документация это прямо и подтверждает: метод просто передает работу в List.sort. Значит ли это, что Comparable больше не нужен? Нет. Естественный порядок это свойство самого класса, и он работает там, где компаратор передать некуда: Arrays.sort(array) и Collections.sort(list) без второго аргумента, TreeSet и TreeMap без компаратора в конструкторе, Collections.max и Collections.min. Правило простое: один очевидный порядок зашивают в класс через Comparable, а все остальные пишут отдельными компараторами.

compareTo и equals должны быть согласованы

Есть требование, о котором легко забыть, а потом долго ловить странное поведение коллекций. Документация Comparable формулирует его так: настоятельно рекомендуется, хотя формально и не требуется, чтобы x.compareTo(y) == 0 давало тот же ответ, что и x.equals(y). В классе User выше это условие нарушено. Метод compareTo смотрит на имя и возраст, а equals в классе не переопределен вовсе и достался от Object, то есть сравнивает ссылки. Получается, что два разных объекта с одинаковыми именем и возрастом для compareTo равны, а для equals нет. На Collections.sort это никак не влияет, поэтому пример выше работает как надо. А вот TreeSet и TreeMap решают, одинаковые элементы или нет, именно по compareTo. Положите в TreeSet двух разных пользователей с совпадающими именем и возрастом, и второй молча не добавится. Если объекты класса планируется хранить в таких коллекциях, equals и hashCode стоит переопределить по тем же полям, по которым сравнивает compareTo.

Итоги

Итак, подведем итог. Если вы сторонник порядка во всем и хотите без лишнего кода расположить ваши объекты в массиве или списке, используйте интерфейс Comparable. Реализация его метода compareTo позволяет достаточно легко встроить механизм естественного упорядочивания объектов вашего класса. Если вам приходится работать с коллекциями и массивами объектов стандартных классов, описанных в библиотеке Java, используйте уже готовые реализации compareTo в этих классах.

Вопросы и ответы

Чем compareTo отличается от equals?

Метод equals отвечает на вопрос, равны ли объекты, а compareTo на вопрос, какой из них больше. Документация Comparable настоятельно рекомендует держать их согласованными: результат x.compareTo(y) == 0 должен совпадать с x.equals(y). Формально это не обязательно, но рассогласование ломает TreeSet и TreeMap, потому что они считают элементы одинаковыми именно по compareTo, а не по equals.

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

Нет, контракт требует только правильного знака. Например, String.compareTo возвращает разность кодов символов в первой несовпавшей позиции, а если строки различаются только длиной, то разность длин. Поэтому проверку пишут как if (a.compareTo(b) меньше нуля), а сравнение результата с конкретным числом это ошибка.

Что будет, если сравнить объекты разных типов?

Если дженерики на месте, компилятор просто не даст этого сделать: Comparable из класса User принимает только User. Но дженерики можно обойти, например сложив разнотипные объекты в массив Object или в сырой List. Тогда проверка сдвинется на время выполнения, и сортировка бросит ClassCastException.

Почему compareTo не стоит писать через вычитание?

Запись вида return this.age - o.age работает, пока числа небольшие, и в этом ее коварство. У типа int вычитание переполняется, поэтому на больших по модулю значениях разных знаков результат получит неправильный знак, а сортировка молча соберется неверно. Безопасный вариант это Integer.compare(this.age, o.age).

Как отсортировать в обратном порядке, если compareTo уже написан?

Трогать compareTo не нужно. Вызов Collections.sort(list, Comparator.reverseOrder()) или list.sort(Comparator.reverseOrder()) перевернет естественный порядок класса. Если список уже отсортирован, достаточно Collections.reverse(list), это дешевле полной пересортировки.

Можно ли реализовать Comparable в классе несколько раз?

Нет. Класс реализует Comparable один раз и задает ровно один естественный порядок. Если порядков нужно несколько, на каждый пишут отдельный Comparator и передают его в sort вторым аргументом. Естественный порядок при этом никуда не девается, он просто игнорируется для этой сортировки.

Читайте также

Комментарии (22)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Денис
Уровень 23
17 февраля 2022, 08:27
Нужно пояснение.
public int compareTo(User o) {
        int result = this.name.compareTo(o.name);

//если имена одинаковые -  сравниваем возраст,
используя метод compareTo из класса Integer

        if (result == 0) {
            result = this.age.compareTo(o.age);
//сравнили возраст, получили какой-то результат
        }
        return result;
//вернули результат в compareTo, а дальше то что?
    }
Что происходит дальше после возвращения результата в compareTo, какой дальнейший алгоритм сортировки?
DregidBackend Developer в Сбер
1 сентября 2022, 12:50
Он возвращает число, это число использует метод sort() для того чтобы рассортировать объекты в списке. То есть, к примеру, есть имя начавшееся на букву А, она вернет наименьшее число в отличии от имени начавшаяся на букву Б. Соответственно самое наименьшее число, получает первый индекс в списке и т.д. P.S. Знаю что скорее тебе это уже не нужно, но другим поможет P.S.S. Я рассказываю от того как понял сам эту статью. Поэтому не нужно, если я не прав кидаться картошкой) Просто поправьте
Wiun
Уровень 16
30 марта 2021, 08:32
не совсем понимаю, что такое
//используем метод compareTo из класса String для сравнения имен
        int result = this.name.compareTo(o.name);
мы сравниваем переменную name из класса с какой переменной name? какое имя будет подставляться в аргумент "о"? и дальше. мы метод compareTo нигде не вызываем, а пользуемся Collections.sort так зачем мы его описывали и переопределяли?
AlexS
Уровень 19
1 апреля 2021, 21:49
мы сравниваем переменную name из класса с какой переменной name? какое имя будет подставляться в аргумент "о"? - с переменной name но уже другого объекта User. Если эти поля равны, то выполняем сравнение возраста. и дальше. мы метод compareTo нигде не вызываем, а пользуемся Collections.sort так зачем мы его описывали и переопределяли? compareTo вызывается всегда когда к нему обращаемся. Или в процессе вывода выполняем сортировку коллекции по нашим правилам, заданным в compareTo. Collections.sort(list); Переопределяли для того, что бы сравнить наших user по именам, и возрасту. Т.е мы собрали из стандартных реализаций compareTo для String, Integer переопределенный compareTo для нашей задачи.
Wiun
Уровень 16
2 апреля 2021, 11:54
как-то еще сложно) спасибо, будем вдумываться)
AlexS
Уровень 19
4 апреля 2021, 12:31
осознание придет,) - Главное задавайте больше вопросов и прогресс будет быстрее
roller
Уровень 36
14 апреля 2024, 06:57
Как я понимаю: public int compareTo() { int result = this.name(поле name объекта у которого мы будем вызывать метод).compareTo(o.name); <--- а это уже объект с которым мы сравниваем. Пример: У объекта User andrey поле name = "Андрей", а у объекта User andrey1 поле name = "Андроид". andrey.compareTo(andrey1); Следовательно результат будет отрицательным числом, т.к поля name у этих объектов разные. P.S Если что, поправьте пожалуйста :)
Viktor Korotkov
Уровень 23
22 декабря 2020, 15:11
Не, ну почему если Андрюхе 44 - он сразу старый?😭
Артем Стукалов
Уровень 23
Expert
5 июля 2020, 17:35
Господи, почему же в гугле нормально всё объясняют с короткими и простыми примерами, а тут через Жопу ?
Daganet
Уровень 22
17 апреля 2020, 22:34
Поясните пж, почему не сортирует по возрасту? получается результат работы метода в этой статье не рабочий.
22 мая 2020, 22:51
все работает. У тебя ошибка. Перечитай статью еще раз. Если не найдешь - пиши в тг @AgentMikle007 за 300 руб покажу где у тебя ошибка)
Миша
Уровень 16
29 мая 2020, 07:35
Вот что карантин с людьми делает
Kirill Badin
Уровень 17
28 июня 2020, 16:28
Результат переопределить забыли, если ещё актуально)
Anonymous #3249457
Уровень 16
Expert
28 мая 2024, 11:04
Не догоняю, он же уже переопределён..?
Igorj4n
Уровень 24
16 апреля 2025, 07:13
В return используется значение переменной result из 36 строки, тогда, когда надо присвоить результат вычисления 37 строки и возвращать его
Denis SidorenkoJava Developer в СберТех
16 марта 2020, 06:21
Например, с помощью реализации этого интерфейса, определены критерии естественного упорядочивания в классах-обертках основных примитивных типов: Byte, Character, Long, Integer, Short, Double, Float, Boolean, String. с каких пор String стал относится к примитивным типам?
MarieBackend Developer в freelance
24 мая 2020, 15:40
> в классах-обёртках
Roman Asanov
Уровень 0
27 мая 2020, 18:56
А String обертка для какого примитива?)
MarieBackend Developer в freelance
29 мая 2020, 16:14
Если уж так хочется докопаться, то в классах-обертках и String
Арсен
Уровень 22
11 февраля 2020, 15:42
приходится. а не приходиться. ПрихОдится работать и приходИться кому-либо братом/сватом.
Владимир
Уровень 0
2 ноября 2018, 17:32
Объясните, пожалуйста. Получается, что при использовании Collections.sort(). Метод compareTo выполняется автоматически? Мы ведь его не вызываем.
Dinara
Уровень 25
1 декабря 2018, 22:25
Да, получается так. Вот тут описание метода Collections.sort() Sorts the specified list into ascending order, according to the natural ordering of its elements. All elements in the list must implement the Comparable interface. Примерный перевод: Сортирует указанный список по возрастанию, согласно естественному порядку расположения его элементов. А реализация интерфейса Comparable как раз задает natural ordering, или естественный порядок расположения, как описано тут (This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class's natural ordering. - Этот интерфейс задает порядок расположения объектов каждого класса, который его реализует. Этот порядок называется естественным порядком расположения.)