Автор
Артем Divertitto
Senior Android-разработчик в United Tech

Comparator в Java

Статья из группы Java Developer
участников
Привет! Сегодня мы поговорим о сравнении объектов. Хм… Но мы, вроде как, уже об этом не раз разговаривали? :/ Мы знаем как работает оператор “==”, а также о методах equals() и hashCode(). Сравнение — не совсем об этом. Раньше мы имели в виду скорее «проверку объектов на равенство». Comparator в Java - 1А у сравнения объектов друг с другом могут быть совсем другие цели! Самая очевидная из них — сортировка. Я думаю, если тебе скажут отсортировать список ArrayList<> чисел или строк, ты с этим справишься без проблем:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       String name1 = "Маша";
       String name2 = "Саша";
       String name3 = "Даша";

       List<String> names = new ArrayList<>();
       names.add(name1);
       names.add(name2);
       names.add(name3);

       Collections.sort(names);
       System.out.println(names);
   }
}
Вывод в консоль:

[Даша, Маша, Саша]
Отлично, если ты вспомнил о классе Collections и его методе sort(). С числами, думаю, тоже проблем не возникнет. А вот тебе задача посложнее:
public class Car {

   private int manufactureYear;
   private String model;
   private int maxSpeed;

   public Car(int manufactureYear, String model, int maxSpeed) {
       this.manufactureYear = manufactureYear;
       this.model = model;
       this.maxSpeed = maxSpeed;
   }

   //...геттеры, сеттеры, toString()

}

import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(2012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);
   }
}
Вот очень просто: класс Car и 3 его объекта. Будь так добр, отсортируй автомобили в списке! Ты наверняка спросишь: «А как их надо отсортировать?». По названию, по году выпуска, по максимальной скорости? Отличный вопрос. Мы не знаем на данный момент, как надо сортировать объекты класса Car. И, вполне естественно, Java тоже этого не знает! При попытке передать в метод Collections.sort() список объектов Car мы получим ошибку:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(20012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);

       //ошибка компилятора!
       Collections.sort(cars);
   }
}
И действительно, откуда языку знать, как именно сортировать написанные тобой объекты? Это зависит от задач твоей программы. Мы должны как-то научить Java сравнивать эти объекты. Причем сравнивать так, как это нужно нам. Для этого в Java есть специальный инструмент — интерфейс Comparable. С английского языка это переводится как «сравнимый». Чтобы наши объекты Car можно было сравнивать друг с другом и как-то сортировать, класс должен имплементировать этот интерфейс и реализовать его единственный метод — compareTo():
public class Car implements Comparable<Car> {

   private int manufactureYear;
   private String model;
   private int maxSpeed;

   public Car(int manufactureYear, String model, int maxSpeed) {
       this.manufactureYear = manufactureYear;
       this.model = model;
       this.maxSpeed = maxSpeed;
   }

   @Override
   public int compareTo(Car o) {
       return 0;
   }

   //...геттеры, сеттеры, toString()

}
Обрати внимание: мы указали интерфейс Comparable<Car>, а не просто Comparable. Это типизированный интерфейс, то есть, требует указания конкретного класса, с которым он связан. В принципе, <Car> можно из интерфейса и убрать, но тогда он по умолчанию сравнивает объекты Object. Вместо метода compareTo(Car o) у нас в классе будет:
@Override
   public int compareTo(Object o) {
       return 0;
   }
Нам, конечно, гораздо проще работать с Car. Внутри метода compareTo() мы реализуем логику сравнения машин. Допустим, нам нужно отсортировать их по году выпуска. Наверное, ты обратил внимание, что метод compareTo() возвращает значение int, а не boolean. Пусть тебя это не удивляет. Дело в том, что сравнение двух объектов дает нам 3 возможных варианта:
  • а < b
  • a > b
  • a == b.
У boolean же есть всего 2 значения — true и false, что неудобно для сравнения объектов. С int все намного проще. Если возвращаемое значение > 0, значит a > b. Если результат compareTo < 0, значит а < b. Ну а если результат == 0, значит два объекта равны: a == b. Научить наш класс сортировать машины по году выпуска — проще простого:
@Override
public int compareTo(Car o) {
   return this.getManufactureYear() - o.getManufactureYear();
}
Что здесь происходит? Мы берем один объект машины (this), год выпуска этой машины и вычитаем из него год выпуска другой машины (той, с которой сравниваем объект). Если год выпуска первой машины больше, метод вернет int > 0. А значит, машина this > машины о. Если наоборот — год выпуска второй машины (о) больше, значит метод вернет отрицательное число, а следовательно, о > this. Ну а если они равны, метод вернет 0. Такого простого механизма уже достаточно, чтобы сортировать коллекции объектов Car! Больше ничего делать не нужно. Вот, пожалуйста:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(2012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);

       //тут раньше была ошибка
       Collections.sort(cars);
       System.out.println(cars);
   }
}
Вывод в консоль:

[Car{manufactureYear=1990, model='Ferrari 360 Spider', maxSpeed=310}, 
Car{manufactureYear=2010, model='Bugatti Veyron', maxSpeed=350}, 
Car{manufactureYear=2012, model='Lamborghini Gallardo', maxSpeed=290}]
Машины отсортированы как надо! :) Comparator в Java - 2В каких случаях надо использовать Comparable? Реализованный в Comparable метод сравнения называют «natural ordering» — естественной сортировкой. Это потому, что в методе compareTo() ты описываешь наиболее распространенный способ сравнения, который будет использоваться для объектов этого класса в твоей программе. Natural Ordering уже присутствует в Java. Например, Java знает, что строки чаще всего сортируют по алфавиту, а числа — по возрастанию их значения. Поэтому если вызвать на списке чисел или строк метод sort(), так они и будут отсортированы. Если в нашей программе машины в большинстве случаев будут сравниваться и сортироваться по году выпуска, значит, стоит определить для них натуральную сортировку с помощью интерфейса Comparable<Car> и метода compareTo(). Но что, если нам этого недостаточно? Давай представим, что наша программа не так проста. В большинстве случаев натуральная сортировка машин (мы установили ее по году выпуска) нас устраивает. Но иногда среди наших клиентов попадаются любители быстрой езды. Если мы готовим для них каталог автомобилей на выбор, их нужно упорядочить по максимальной скорости. Comparator в Java - 3К примеру, такая сортировка нам нужна в 15% случаев. Этого явно недостаточно, чтобы установить натуральную сортировку для Car по скорости вместо года выпуска. Но и игнорировать 15% клиентов мы не можем. Что же нам делать? Здесь нам приходит на помощь другой интерфейс — Comparator. Так же, как и Comparable, он типизированный. А в чем же разница? Comparable делает наши объекты «сравнимыми» и создает для них наиболее естественный порядок сортировки, который будет использоваться в большинстве случаев. Comparator — это отдельный класс-«сравниватель» (перевод немного корявый, но зато понятный). Если нам нужно реализовать какую-то специфическую сортировку, нам необязательно лезть в класс Car и менять логику compareTo(). Вместо этого мы можем создать отдельный класс-comparator в нашей программе и научить его делать нужную нам сортировку!
import java.util.Comparator;

public class MaxSpeedCarComparator implements Comparator<Car> {

   @Override
   public int compare(Car o1, Car o2) {
       return o1.getMaxSpeed() - o2.getMaxSpeed();
   }
}
Как видишь, наш Comparator довольно прост. Всего один метод compare() — это метод интерфейса Comparator, который обязательно нужно реализовать. Он принимает на вход два объекта Car и привычным нам образом (вычитанием) сравнивает их максимальную скорость. Как и compareTo(), он возвращает число int, принцип сравнения тот же. Как же нам пользоваться этим? Очень просто:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(2012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);

       Comparator speedComparator = new MaxSpeedCarComparator();
       Collections.sort(cars, speedComparator);

       System.out.println(cars);
   }
}
Вывод в консоль:

[Car{manufactureYear=2012, model='Lamborghini Gallardo', maxSpeed=290}, 
Car{manufactureYear=1990, model='Ferrari 360 Spider', maxSpeed=310}, 
Car{manufactureYear=2010, model='Bugatti Veyron', maxSpeed=350}]
Мы просто создаем объект-компаратор и передаем его в метод Collections.sort() вместе со списком, который надо отсортировать. Получив на вход компаратор, метод sort() не будет использовать естественную сортировку, определенную в методе compareTo() класса Car. Вместо этого он применит алгоритм сортировки из переданного ему компаратора. Какие плюсы нам это дает? Во-первых, совместимость с написанным кодом. Мы создали новый, специфический метод сортировки, и при этом сохранили действующий, который будет использоваться в большинстве случаев. Мы вообще не трогали класс Car. Он как был Comparable, так и остался:
public class Car implements Comparable<Car> {

   private int manufactureYear;
   private String model;
   private int maxSpeed;

   public Car(int manufactureYear, String model, int maxSpeed) {
       this.manufactureYear = manufactureYear;
       this.model = model;
       this.maxSpeed = maxSpeed;
   }

   @Override
   public int compareTo(Car o) {
       return this.getManufactureYear() - o.getManufactureYear();
   }

   //...геттеры, сеттеры, toString()

}
Во-вторых, гибкость. Мы можем добавлять сколько угодно сортировок. Скажем, отсортировать машины по цвету, скорости, весу, или по тому, сколько раз эта машина использовалась в фильмах про Бэтмена. Достаточно только создать дополнительный Comparator. Вот и все! Сегодня ты изучил два очень важных механизма, которые часто будешь использовать в реальных проектах на работе. Но, как известно, теория без практики — ничто. Поэтому, самое время закрепить знания и решить несколько задач! :)
Комментарии (53)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Алексей
Уровень 40
Expert
7 февраля, 19:09
Пацан пиши в том же духе)
Sergey
Уровень 1
24 августа 2023, 06:24
Самая понятная статья, дающая четкое и внятное понимание разницы интерфейсов, и их использования. Интерфейсы поданы под углом, позволяющим быстро уловить соль вопроса. Столько прочитал и везде пишут казенным языком формальные вещи типа такого: "Это - окно, а это - дверь. И то и другое - выход на улицу. Как хочешь так и выходи.".
Майя
Уровень 35
4 июля 2023, 21:21
понятно в чем разница! хорошее объяснение
Aleksa Aleks
Уровень 6
30 апреля 2023, 18:02
Хорошо написана статья, но хочется продолжения! Согласна с DagothUr, как быть с String?
DagothUr
Уровень 15
23 февраля 2023, 19:33
Вот показали, как с числовыми значениями, а с String? По hash сортировать что ли String?
Никто и звать меня никак
16 января 2023, 14:13
Нифига не пойму... Так с Comparable тоже можно сравнить по скорости. Так в чем разница сравнения между Comparable и Comparator? Зачем мне создавать отдельный класс для сравнения того, что можно сравнить с помощью compareTo()? Уже третий день ищу разницу. И везде пишут, "Нуууу, с компарабл можно сравнить по скорости естественной сортировкой, а вот с компаратором зато можно сравнить по скорости неестественной соритровкой, ведь числа то нельзя сравнивать естественной, что вы!" Где разница то?
Джонни
Уровень 51
16 января 2023, 14:39
Если нам нужно реализовать какую-то специфическую сортировку, нам необязательно лезть в класс Car и менять логику compareTo().
В случае с Comparable у вас есть один метод, неважно какую логику вы в него добавите. Будь то сравнение по скорости/дате выпуска/названию или же все перечисленное. Метод будет работать только так. А если вам надо сейчас отсортировать по году, а следующим "запросом" по скорости? Потом попросят по производителю? Каждый раз лезть и переписывать логику метода? Нет, можно просто, вызывая
Collections.sort(list, comparator);
Подставлять нужный компаратор
Comparator<Car> compareByMaxSpeed = new Comparator<Car>() {
            @Override
            public int compare(Car o1, Car o2) {
                return o1.getMaxSpeed() - o2.getMaxSpeed();
            }
        };
Comparator<Car> compareByManufactureYear = new Comparator<Car>() {
            @Override
            public int compare(Car o1, Car o2) {
                return o1.getManufactureYear() - o2.getManufactureYear();
            }
        };
Никто и звать меня никак
16 января 2023, 14:49
Тогда получается, что Comparable нужно использовать только там где ты 100% знаешь, что никаких других сортировок клиент не потребует? Comparable не дает выбора сортировки, потому что он один? А Comparator лишь дает разнообразие сортировки? Создал кучу Comparator'ов на любой вкус и цвет и пусть клиент сам сортирует что он хочет? Но при чем тут "натуральная сортировка"? Оно же и так понятно, что-бы ты не сделал - сортировка будет натуральной и никакой больше(даже на цвета и запах твой взгляд создаст натуральную сортировку). Натуральность она же субъективная.
Джонни
Уровень 51
16 января 2023, 15:07
Тогда получается, что Comparable нужно использовать только там где ты 100% знаешь, что никаких других сортировок клиент не потребует?
Не потребует кто-то, не понадобится вам же в вашем коде. Банально большинство базовых списков с людьми сортируется по фамилии, в таком случае для нашего List<User> users хватит реализации Comparable<User> в классе User.
Comparable не дает выбора сортировки, потому что он один?
Метод да, один. Можете изучить в официальной документации.
А Comparator лишь дает разнообразие сортировки? Создал кучу Comparator'ов на любой вкус и цвет и пусть клиент сам сортирует что он хочет?
Вариативность и гибкость, всё верно.
Но при чем тут "натуральная сортировка"? Оно же и так понятно, что-бы ты не сделал - сортировка будет натуральной и никакой больше(даже на цвета и запах твой взгляд создаст натуральную сортировку). Натуральность она же субъективная.
Вы можете ознакомиться с понятием "натуральной" сортировки немного погуглив. Для примера вот определение с википедии (на мой взгляд удачный там пример).
Kurama
Уровень 50
11 ноября 2022, 21:05
Хоть мы и приходили это кучу раз, но не забываем про сахар:
Collections.sort(cars, (o1, o2) -> o1.getMaxSpeed() - o2.getMaxSpeed());
или вообще
cars.sort((o1, o2) -> o1.getMaxSpeed() - o2.getMaxSpeed());
Andrey Chuev
Уровень 43
21 ноября 2022, 09:51
Больше сахару) 😁
cars.sort(Comparator.comparingInt(Car::getMaxSpeed));
Kurama
Уровень 50
21 ноября 2022, 18:26
Очень даже неплохо, это точно из лекций? Не помню просто тут такого
Andrey Chuev
Уровень 43
22 ноября 2022, 07:30
Ссылки на методы были ещё в Java Syntax)
пумпурум
Уровень 7
19 апреля 2023, 12:56
спасибо за сахар))
Kapinos
Уровень 38
2 декабря 2023, 17:48
И еще немного Collections.sort(list, Car::compareTo); или list.stream().sorted(Car::compareTo);
11 октября 2022, 11:12
тут вроде ошибка, public class MaxSpeedCarComparator должен быть STATIC. в таком виде как здесь его объект не создается в MAIN.
DvS LoPPo
Уровень 2
4 октября 2022, 17:51
Cупер. Настоящий наставник
YesOn
Уровень 13
10 сентября 2022, 01:37
Крутая статья! Очень понятные примеры и объяснение! Спасибо, очень пригодилось!🙂👍