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

Интерфейсы 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. В этом случае вы можете комбинировать несколько полей для сравнения и даже менять порядок сортировки на обратный.



Комментарии

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

×

devmark.ru