10 июня 2024
Тэги: Collections, Java, ООП.
Интерфейсы Comparable и Comparator, как и Iterable и Iterator (см. Перебор элементов через Iterator), являются частью Java Collections. Подобно тому, как Iterable наделяет объекты свойством перебора в цикле, так и Comparable («сравниваемый») наделяет объекты свойством сравнимости между собой.
Данный интерфейс предоставляет универсальный способ сравнивать два объекта между собой. Его контракт состоит из единственного метода compareTo():
Метод сравнивает текущий объект this с другим объектом того же типа other, который передали в параметре. В качестве результата метод возвращает целое число. Этот результат определяется контрактом данного метода и должен принимать одно из следующих значений:
Из этих правил можно вывести следующую рекомендацию. Если вы хотите сравнивать два объекта A и B, то сравнивайте результат работы метода a.compareTo(b) с нулём. Причём ставьте между результатом и нулём такой же оператор сравнения, какой вы ставили бы между A и B.
Если какой-то класс реализует интерфейс сравнения, то коллекция из таких объектов может быть отсортирована, в ней уже можно искать минимум или максимум с помощью стандартных методов Java Collection.
В Java многие стандартные классы реализуют интерфейс Comparable, например:
и т.д.
Рассмотрим пример, как можно использовать интерфейс Comparable и метод compareTo() в Java. Предположим, что мы создали класс Person, который содержит имя и возраст. Мы хотим, чтобы объекты класса Person можно было сравнивать по возрасту.
В этом примере для удобства мы использовали record class. Компилятор автоматически сгенерирует конструктор с параметрами, геттеры и сеттеры, а также метод toString(). В этом классе мы реализовали интерфейс Comparable и метод compareTo(), который сравнивает объекты только по возрасту. Мы использовали вспомогательный метод Integer.compare(), который сравнивает два целых числа и возвращает результат согласно рассмотренному нами контракту.
Интересно, что для числовых типов реализацию сравнения можно сделать ещё проще. Достаточно из this вычесть other:
И такая реализация будет удовлетворять контракту метода. Действительно, если два числа равны, то их разница будет нулевой. Если this больше other – разница будет положительной, а если меньше – отрицательной. Но всё же лучше использовать более универсальные стандартные реализации сравнения.
Теперь мы можем создать список объектов Person и отсортировать их по возрасту:
Также мы можем использовать интерфейс Comparable и метод compareTo() для сравнения объектов в других ситуациях, например, в поиске минимального и максимального элементов:
В этом примере мы использовали статические методы min() и max() из класса Collections, которые находят минимальный и максимальный элементы в коллекции.
Некоторые виды коллекций, такие как TreeMap и построенный на её основе TreeSet (см. Коллекции: list, set, map), требуют реализации интерфейса Comparable. Это связано с тем, что они автоматически сортируют свои элементы и им очень важно знать, по какому полю производить сортировку.
Из всего вышесказанного можно сделать вывод, что интерфейс Comparable определяет порядок сортировки по умолчанию. Но как быть, если в одном случае нам нужно сортировать по возрасту, а в другом – по имени? На помощь нам придёт интерфейс Comparator (дословно «сравниватель»), который принимает на вход два объекта для сравнения.
Тогда мы можем создать отдельный компаратор для сравнения тех же Person по имени.
Тогда при сортировке списка достаточно передать этот компаратор в метод sort(), чтобы изменить порядок сортировки.
Мы можем создать сколько угодно таких компараторов для класса Person на все случаи жизни.
Если в вашем проекте достаточно большое количество сущностей и для каждой из них необходимо поддерживать несколько вариантов сортировок, определять под каждый случай отдельный компаратор может оказаться очень накладно. Для этих целей можно создавать компараторы «на лету» с помощью статических методов интерфейса Comparator и лямбда-выражений.
Предыдущий пример с сортировкой по имени можно свести к следующему:
Здесь мы в статический метод comparing() передаём лямбду с указанием того поля, по которому хотим сортировать. Точно так же можно сделать сортировку по возрасту.
Мы можем так же легко сделать обратную сортировку по имени с помощью метода reversed():
Ну и ещё мы можем сортировать по нескольким полям. Например, если сортируем сначала по возрасту и возраст одинаковый, то затем мы можем сортировать по имени, чтобы при одинаковом возрасте первым оказался пользователь с именем, идущим раньше по алфавиту.
Здесь мы используем метод thenComparing() для сортировки по имени после вызова comparing() для сортировки по возрасту. И так можно комбинировать сколько угодно полей.
Интерфейс Comparable задаёт правила сортировки по умолчанию. Объекты, реализующие этот интерфейс, уже могут сравниваться с помощью стандартных методов сортировки, поиска минимума и максимума. Такие коллекции как TreeMap и TreeSet требуют реализацию интерфейса Comparable, т.к. автоматически сортируют элементы.
Если вам нужно поддерживать несколько различных вариантов сортировки, используйте интерфейс Comparator. Он позволяет объявлять отдельные классы-компараторы под каждый вид сортировки.
Но гораздо удобнее конструировать компараторы «на лету», используя статический метод comparing() интерфейса Comparator. В этом случае вы можете комбинировать несколько полей для сравнения и даже менять порядок сортировки на обратный.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.