Comparator в Java это интерфейс, который задает правило сортировки объектов и живет в отдельном классе. У него один метод, compare(o1, o2): он возвращает положительное число, отрицательное или ноль. Его напарник Comparable с методом compareTo() встраивает порядок сортировки прямо в сам класс. Comparable задает один порядок по умолчанию, а компараторов к одному классу можно написать сколько угодно.

Кратко

  • Java не умеет сортировать твои классы сама: ей нужно объяснить, какое поле с каким сравнивать.
  • Comparable реализуют внутри самого класса, метод compareTo() задает естественный порядок (natural ordering).
  • Comparator это отдельный класс с методом compare(). Исходный класс при этом менять не нужно.
  • Оба метода возвращают int, а не boolean: больше нуля, меньше нуля или ноль, потому что у сравнения три исхода, а не два.
  • Collections.sort(list) использует compareTo(), а Collections.sort(list, comparator) берет правило из переданного компаратора.
  • Компараторов к одному классу можно завести сколько угодно: по скорости, по цвету, по чему угодно, и старый код от этого не сломается.
Привет! Сегодня мы поговорим о сравнении объектов. Хм… Но мы, вроде как, уже об этом не раз разговаривали? :/ Мы знаем как работает оператор “==”, а также о методах equals() и hashCode(). Сравнение это не совсем об этом. Раньше мы имели в виду скорее «проверку объектов на равенство». Два ретро-автомобиля Volkswagen Beetle стоят друг напротив друга в поле: сравниваем объекты между собойА у сравнения объектов друг с другом могут быть совсем другие цели! Самая очевидная из них это сортировка. Я думаю, если тебе скажут отсортировать список 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(). С числами, думаю, тоже проблем не возникнет.

Почему Java не может отсортировать твои объекты сама

А вот тебе задача посложнее:

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(2012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

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

       //ошибка компилятора!
       Collections.sort(cars);
   }
}
И действительно, откуда языку знать, как именно сортировать написанные тобой объекты? Это зависит от задач твоей программы. Мы должны как-то научить Java сравнивать эти объекты. Причем сравнивать так, как это нужно нам.

Интерфейс Comparable и метод compareTo()

Для этого в 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

Наверное, ты обратил внимание, что метод compareTo() возвращает значение int, а не boolean. Пусть тебя это не удивляет. Дело в том, что сравнение двух объектов дает нам 3 возможных варианта:
  • a < b
  • a > b
  • a == b.
У boolean же есть всего 2 значения, true и false, что неудобно для сравнения объектов. С int все намного проще. Если возвращаемое значение > 0, значит a > b. Если результат compareTo < 0, значит a < b. Ну а если результат == 0, значит два объекта равны: a == b.

Сортировка по году выпуска

Научить наш класс сортировать машины по году выпуска это проще простого:

@Override
public int compareTo(Car o) {
   return this.getManufactureYear() - o.getManufactureYear();
}
Что здесь происходит? Мы берем один объект машины (this), год выпуска этой машины и вычитаем из него год выпуска другой машины (той, с которой сравниваем объект). Если год выпуска первой машины больше, метод вернет int > 0. А значит, машина this > машины o. Если наоборот, год выпуска второй машины (o) больше, значит метод вернет отрицательное число, а следовательно, o > 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}]
Машины отсортированы как надо! :) Ряд автомобилей BMW в музее, выстроенных по поколениям: машины отсортированы по году выпуска

Natural ordering: естественная сортировка

В каких случаях надо использовать Comparable? Реализованный в Comparable метод сравнения называют «natural ordering», то есть естественной сортировкой. Это потому, что в методе compareTo() ты описываешь наиболее распространенный способ сравнения, который будет использоваться для объектов этого класса в твоей программе. Natural Ordering уже присутствует в Java. Например, Java знает, что строки чаще всего сортируют по алфавиту, а числа по возрастанию их значения. Поэтому если вызвать на списке чисел или строк метод sort(), так они и будут отсортированы. Если в нашей программе машины в большинстве случаев будут сравниваться и сортироваться по году выпуска, значит, стоит определить для них натуральную сортировку с помощью интерфейса Comparable<Car> и метода compareTo(). Но что, если нам этого недостаточно? Давай представим, что наша программа не так проста. В большинстве случаев натуральная сортировка машин (мы установили ее по году выпуска) нас устраивает. Но иногда среди наших клиентов попадаются любители быстрой езды. Если мы готовим для них каталог автомобилей на выбор, их нужно упорядочить по максимальной скорости. Два спорткара крупным планом, темный Mustang и красный GT-R: этим клиентам нужна сортировка по максимальной скоростиК примеру, такая сортировка нам нужна в 15% случаев. Этого явно недостаточно, чтобы установить натуральную сортировку для Car по скорости вместо года выпуска. Но и игнорировать 15% клиентов мы не можем. Что же нам делать?

Интерфейс Comparator: отдельный класс-сравниватель

Здесь нам приходит на помощь другой интерфейс, 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.

Comparable и Comparator: в чем разница

Соберем оба интерфейса из статьи в одну таблицу:
СвойствоComparableComparator
Где живет логика сравнениявнутри самого классав отдельном классе-компараторе
МетодcompareTo(Car o), один аргументcompare(Car o1, Car o2), два аргумента
Сколько вариантов сортировкиодин, естественный порядоксколько угодно, по одному классу на каждый
Нужно ли менять исходный классда, он должен реализовать интерфейснет, класс остается нетронутым
Как применяетсяCollections.sort(cars)Collections.sort(cars, comparator)
Вот и все! Сегодня ты изучил два очень важных механизма, которые часто будешь использовать в реальных проектах на работе. Но, как известно, теория без практики это ничто. Поэтому, самое время закрепить знания и решить несколько задач! :)

Вопросы и ответы

Чем Comparable отличается от Comparator?

Comparable реализуют внутри самого класса, он задает один естественный порядок сортировки. Comparator это отдельный класс с методом compare(), и таких классов к одному типу можно написать сколько угодно, не трогая исходный класс.

Почему compareTo() возвращает int, а не boolean?

Потому что у сравнения три исхода, а не два: первый объект больше, меньше или равен второму. Число больше нуля означает, что текущий объект больше, меньше нуля что меньше, ноль что они равны.

Что такое natural ordering?

Это порядок сортировки по умолчанию, заданный в самом классе через compareTo(). У строк это алфавитный порядок, у чисел по возрастанию. Именно его использует Collections.sort() без дополнительных аргументов.

Почему Collections.sort() не работает с моим классом?

Потому что класс не реализует Comparable, и Java не знает, по какому полю сравнивать объекты. Нужно либо реализовать Comparable с методом compareTo(), либо передать вторым аргументом готовый компаратор.

Как отсортировать один и тот же список несколькими способами?

Написать по отдельному классу-компаратору на каждый вариант сортировки и передавать нужный в Collections.sort(). Естественный порядок из compareTo() при этом сохраняется и продолжает работать там, где компаратор не передан.

Почему сравнение вычитанием бывает опасным?

Для годов и скоростей, как в примерах статьи, вычитание работает нормально. Но на больших значениях разность может переполнить int и дать неверный знак. В таких случаях надежнее Integer.compare(a, b): он возвращает тот же результат без риска переполнения. Источник: Oracle Java Tutorials: Object Ordering.

Читайте также