Статьи
Утилиты Telegram YouTube Отзывы

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

Видеогайд

10 июня 2024

Тэги: Collections, Java, ООП.

Содержание

  1. Интерфейс Comparable
  2. Примеры реализации интерфейса Comparable
  3. Интерфейс Comparator
  4. Комбинированные компараторы
  5. Выводы

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

Интерфейсы Comparable и Comparator, как и Iterable и Iterator (см. Перебор элементов через Iterator), являются частью Java Collections. Подобно тому, как Iterable наделяет объекты свойством перебора в цикле, так и Comparable («сравниваемый») наделяет объекты свойством сравнимости между собой.

Интерфейс Comparable

Данный интерфейс предоставляет универсальный способ сравнивать два объекта между собой. Его контракт состоит из единственного метода compareTo():

interface Comparable<T> {
    int compareTo(T other);
}

Метод сравнивает текущий объект this с другим объектом того же типа other, который передали в параметре. В качестве результата метод возвращает целое число. Этот результат определяется контрактом данного метода и должен принимать одно из следующих значений:

  • 0 – если объекты равны
  • положительное число – если this > other
  • отрицательное число – если this < other

Из этих правил можно вывести следующую рекомендацию. Если вы хотите сравнивать два объекта A и B, то сравнивайте результат работы метода a.compareTo(b) с нулём. Причём ставьте между результатом и нулём такой же оператор сравнения, какой вы ставили бы между A и B.

a.compareTo(b) > 0 // A > B
a.compareTo(b) == 0 // A == B
a.compareTo(b) <= 0 // A <= B

Если какой-то класс реализует интерфейс сравнения, то коллекция из таких объектов может быть отсортирована, в ней уже можно искать минимум или максимум с помощью стандартных методов Java Collection.

В Java многие стандартные классы реализуют интерфейс Comparable, например:

  • Integer
  • Double
  • String
  • Date
  • LocalDate
  • BigInteger
  • BigDecimal

и т.д.

Примеры реализации интерфейса Comparable

Рассмотрим пример, как можно использовать интерфейс Comparable и метод compareTo() в Java. Предположим, что мы создали класс Person, который содержит имя и возраст. Мы хотим, чтобы объекты класса Person можно было сравнивать по возрасту.

public record Person(
        String name,
        int age
) implements Comparable<Person> {
    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

В этом примере для удобства мы использовали record class. Компилятор автоматически сгенерирует конструктор с параметрами, геттеры и сеттеры, а также метод toString(). В этом классе мы реализовали интерфейс Comparable и метод compareTo(), который сравнивает объекты только по возрасту. Мы использовали вспомогательный метод Integer.compare(), который сравнивает два целых числа и возвращает результат согласно рассмотренному нами контракту.

Интересно, что для числовых типов реализацию сравнения можно сделать ещё проще. Достаточно из this вычесть other:

public int compareTo(Person other) {
    return this.age - other.age;
}

И такая реализация будет удовлетворять контракту метода. Действительно, если два числа равны, то их разница будет нулевой. Если this больше other – разница будет положительной, а если меньше – отрицательной. Но всё же лучше использовать более универсальные стандартные реализации сравнения.

Теперь мы можем создать список объектов Person и отсортировать их по возрасту:

public static void main(String[] args) {
  List<Person> persons = Arrays.asList(
          new Person("Антон", 30),
          new Person("Ярослав", 25),
          new Person("Василий", 20)
  );
  Collections.sort(persons);
  System.out.println(persons);
  // [Person[name=Василий, age=20], Person[name=Ярослав, age=25], Person[name=Антон, age=30]]
}

Также мы можем использовать интерфейс Comparable и метод compareTo() для сравнения объектов в других ситуациях, например, в поиске минимального и максимального элементов:

Person youngestPerson = Collections.min(persons);
System.out.printf("Самый молодой: %s%n", youngestPerson.name());  // Василий

Person oldestPerson = Collections.max(persons);
System.out.printf("Самый старый: %s%n", oldestPerson.name()); // Антон

В этом примере мы использовали статические методы min() и max() из класса Collections, которые находят минимальный и максимальный элементы в коллекции.

Некоторые виды коллекций, такие как TreeMap и построенный на её основе TreeSet (см. Коллекции: list, set, map), требуют реализации интерфейса Comparable. Это связано с тем, что они автоматически сортируют свои элементы и им очень важно знать, по какому полю производить сортировку.

Интерфейс Comparator

Из всего вышесказанного можно сделать вывод, что интерфейс Comparable определяет порядок сортировки по умолчанию. Но как быть, если в одном случае нам нужно сортировать по возрасту, а в другом – по имени? На помощь нам придёт интерфейс Comparator (дословно «сравниватель»), который принимает на вход два объекта для сравнения.

interface Comparator<T> {
    int compare(T left, T right);
}

Тогда мы можем создать отдельный компаратор для сравнения тех же Person по имени.

public class PersonByNameComparator implements Comparator<Person> {
    @Override
    public int compare(Person left, Person right) {
        return left.name().compareTo(right.name());
    }
}

Тогда при сортировке списка достаточно передать этот компаратор в метод sort(), чтобы изменить порядок сортировки.

public static void main(String[] args) {
  List<Person> persons = Arrays.asList(
          new Person("Антон", 30),
          new Person("Ярослав", 25),
          new Person("Василий", 20)
  );
  Collections.sort(persons, new PersonByNameComparator());
  // или persons.sort(new PersonByNameComparator());
  System.out.println(persons);
  // [Person[name=Антон, age=30], Person[name=Василий, age=20], Person[name=Ярослав, age=25]]
}

Мы можем создать сколько угодно таких компараторов для класса Person на все случаи жизни.

Комбинированные компараторы

Если в вашем проекте достаточно большое количество сущностей и для каждой из них необходимо поддерживать несколько вариантов сортировок, определять под каждый случай отдельный компаратор может оказаться очень накладно. Для этих целей можно создавать компараторы «на лету» с помощью статических методов интерфейса Comparator и лямбда-выражений.

Предыдущий пример с сортировкой по имени можно свести к следующему:

public static void main(String[] args) {
  List<Person> persons = Arrays.asList(
          new Person("Антон", 30),
          new Person("Ярослав", 25),
          new Person("Василий", 20)
  );
  Comparator<Person> personByNameComparator = Comparator.comparing(Person::name);
  persons.sort(personByNameComparator);
  System.out.println(persons);
  // [Person[name=Антон, age=30], Person[name=Василий, age=20], Person[name=Ярослав, age=25]]
}

Здесь мы в статический метод comparing() передаём лямбду с указанием того поля, по которому хотим сортировать. Точно так же можно сделать сортировку по возрасту.

Comparator<Person> personByAgeComparator = Comparator.comparing(Person::age);
persons.sort(personByAgeComparator);
// [Person[name=Василий, age=20], Person[name=Ярослав, age=25], Person[name=Антон, age=30]]

Мы можем так же легко сделать обратную сортировку по имени с помощью метода reversed():

Comparator<Person> personByNameReversedComparator = Comparator.comparing(Person::name).reversed();
persons.sort(personByNameReversedComparator);
// [Person[name=Ярослав, age=25], Person[name=Василий, age=20], Person[name=Антон, age=30]]

Ну и ещё мы можем сортировать по нескольким полям. Например, если сортируем сначала по возрасту и возраст одинаковый, то затем мы можем сортировать по имени, чтобы при одинаковом возрасте первым оказался пользователь с именем, идущим раньше по алфавиту.

public static void main(String[] args) {
    List<Person> persons = Arrays.asList(
            new Person("Антон", 30),
            new Person("Ярослав", 25),
            new Person("Василий", 25)
    );
    Comparator<Person> personByAgeAndNameComparator = Comparator
            .comparing(Person::age)
            .thenComparing(Person::name);
    persons.sort(personByAgeAndNameComparator);
    System.out.println(persons);
    // [Person[name=Василий, age=25], Person[name=Ярослав, age=25], Person[name=Антон, age=30]]
}

Здесь мы используем метод 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.

Последние статьи


Комментарии

Добавить комментарий

×

devmark.ru