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() класса
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 вторым аргументом. Естественный порядок при этом никуда не девается, он просто игнорируется для этой сортировки.
Читайте также
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ