В этой статье мы посмотрим с вами на то, как устроен интерфейс Comparable в Java, и научимся сравнивать два объекта, которые реализуют этот интерфейс.
Прежде, чем мы начнём погружение в эту тему и рассмотрим конкретный пример реализации, я скажу пару слов об этом интерфейсе. Он появился, начиная с версии Java 1.2, и является частью Java Collections Framework. У него есть всего один-единственный метод:
public int compareTo(T o);
Ссылка на официальную документацию для версии Java 17: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Comparable.html
Этот интерфейс накладывает полный порядок (total ordering) на объекты каждого класса, который его реализует, и этот порядок называется естественным порядком класса (class's natural ordering), а метод compareTo(), который предоставляется интерфейсом Comparable, называется его естественным методом сравнения.
Списки и массивы объектов, реализующих этот интерфейс, могут автоматически сортироваться с помощью Collections.sort и Arrays.sort.
Объекты, реализующие этот интерфейс, могут использоваться как ключи в отсортированной карте (sorted map) или как элементы в отсортированном наборе (sorted set) без необходимости указывать компаратор.
Также нужно учитывать, что null не является экземпляром какого-либо класса, и e.compareTo(null) должен генерировать исключение NullPointerException, даже если e.equals(null) возвращает false.
Давайте теперь посмотрим на пример того, как работать с интерфейсом Comparable в Java и как можно реализовать его единственный метод compareTo(). В нашем примере мы создадим довольно простой класс с названием GiftBox, описывающий подарочную коробку. Наш класс будет реализовывать интерфейс Comparable, а также его метод compareTo(). У коробки будет какое-то название (поле name), а также её длина (поле length), ширина (поле width) и высота (поле height). Из школьного курса математики мы знаем, что объём параллелепипеда V (которым и является, по сути, наша подарочная коробка) вычисляется по формуле:
V = a × b × c
, где a, b и c - длина, ширина и высота параллелепипеда. Поэтому в нашем классе мы также определим с вами метод getVolume(), который будет вычислять объём нашей подарочной коробки. У класса также будет конструктор с параметрами, который сможет создавать экземпляр новой подарочной коробки с указанным названием, а также шириной, высотой и длиной коробки. Ниже представлено описание нашего класса GiftBox:
package ru.allineed.samples.comparable;
/**
* [RU] Класс описывает подарочную коробку с характеристиками ширины (width), длины (length) и высоты (height)
* в сантиметрах<br/>
* [EN] Class describes a gift box with characteristics of width, length and height in centimeters
*/
public class GiftBox implements Comparable<GiftBox> {
/**
* [RU] Название подарочной коробки;<br/>
* [EN] The name of the gift box
*/
private final String name;
/**
* [RU] Ширина коробки (см);<br/>
* [EN] The width of the gift box (cm)
*/
private final int width;
/**
* [RU] Высота коробки (см);<br/>
* [EN] The height of the gift box (cm)
*/
private final int height;
/**
* [RU] Длина коробки (см);<br/>
* [EN] The length of the gift box (cm)
*/
private final int length;
/**
* [RU] Конструктор с параметрами, создающий коробку по заданной ширине (width), высоте (height) и длине (length)<br/>
* [EN] Constructor with parameters that creates a new gift box by specified width, height and length
* @param width [RU] ширина коробки; [EN] the width of the box
* @param height [RU] высота коробки; [EN] the height of the box
* @param length [RU] длина коробки; [EN] the length of the box
*/
public GiftBox(String name, int width, int height, int length) {
this.name = name;
this.width = width;
this.height = height;
this.length = length;
}
/**
* [RU] Возвращает название подарочной коробки;<br/>
* [EN] Returns the name of the gift box
*/
public String getName() {
return name;
}
/**
* [RU] Возвращает ширину подарочной коробки;<br/>
* [EN] Returns the width of the gift box
*/
public int getWidth() {
return width;
}
/**
* [RU] Возвращает высоту подарочной коробки;<br/>
* [EN] Returns the height of the gift box
*/
public int getHeight() {
return height;
}
/**
* [RU] Возвращает длину подарочной коробки;<br/>
* [EN] Returns the length of the gift box
*/
public int getLength() {
return length;
}
/**
* [RU] Вычисляет и возвращает объём коробки (куб. см.)<br/>
* [EN] Calculates and returns the volume of the box (cubic cm)
* @return [RU] целое число, равное объему коробки; [EN] an integer representing the volume of the box
*/
public int getVolume() {
return height * width * length;
}
/**
* [RU] Переопределённый метод интерфейса {@link Comparable} для сравнения двух коробок - текущей и
* переданной в параметре 'that'<br/>
* [EN] An overridden method of {@link Comparable} interface for comparing two boxes - the current box
* instance and another that is passed in 'that' input parameter
* @param that [RU] объект коробки для сравнения с текущим; [EN] the object to be compared with the current instance
* @return [RU] отрицательное значение -1, если текущая коробка меньше переданной в параметре 'that',
* 0, если текущая коробка равна переданной в параметре 'that', 1, если текущая коробка больше переданной в
* параметре 'that'; [EN] a negative integer value -1 in case current box is less than the box in the parameter
* 'that', 0 in case current box is equal to 'that' box, 1 in case current box is bigger than 'that' box.
* @throws NullPointerException [RU] если {@code that} равен null; [EN] if {@code that} is null
*/
@Override
public int compareTo(GiftBox that) {
int thatVolume = that.getVolume();
int thisVolume = getVolume();
if (thisVolume < thatVolume) {
return -1;
} else if (thisVolume > thatVolume) {
return 1;
}
return 0;
}
/**
* [RU] Преобразует текущий экземпляр подарочной коробки в строковое представление<br/>
* [EN] Transforms the current instance of the gift box into the string representation
* @return [RU] строка, содержащая детали о текущем экземпляре подарочной коробки;
* [EN] the string containing the details about the current gift box instance
*/
@Override
public String toString() {
return "GiftBox{" +
"name='" + name + "'" +
", width=" + width +
", height=" + height +
", length=" + length +
'}';
}
}
Обратите внимание на метод compareTo() и то, как именно мы его переопределяем для интерфейса Comparable:
- в случае, если в метод передаётся ссылка на пустой объект, т.е. that == null, мы выбрасываем исключение NullPointerException.
- если вычисленный объём текущего экземпляра подарочной коробки меньше, чем объём данного в параметре that экземпляра другой подарочной коробки, то мы возвращаем отрицательное число -1
- если вычисленный объём текущего экземпляра подарочной коробки больше, чем объём данного в параметре that экземпляра другой подарочной коробки, то мы возвращаем положительное число 1
- если же объемы двух подарочных коробок (текущего экземпляра и данного в параметре that) одинаковы, то мы вернём 0.
Для упрощения примера и работы метода compareTo() мы не будем смотреть на поле name с названием подарочной коробки - т.е. решение о том, равны ли коробки или какая-то из них больше другой будем принимать только на основании размеров коробок, т.е. их объёмов.
Теперь можем написать простой код, который продемонстрирует то, как можно сравнивать между собой различные коробки. Создадим рядом с классом GiftBox класс ComparableGiftBoxExample, который будет иметь метод main() - для возможности запуска нашего примера, и напишем в нём следующий код:
package ru.allineed.samples.comparable;
import ru.allineed.samples.config.Localization;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ComparableGiftBoxExample {
public static void main(String[] args) {
// [RU] создаём экземпляр красной коробки
// [EN] create an instance of the red box
GiftBox redBox = new GiftBox("Red Box", 100, 100, 100);
// [RU] создаём экземпляр синей коробки
// [EN] create an instance of the blue box
GiftBox blueBox = new GiftBox("Blue Box", 30, 60, 100);
// [RU] сравниваем синюю коробку с красной
// [EN] comparing the blue box with the red one
int comparisonResult = blueBox.compareTo(redBox);
// [RU] распечать результаты сравнения
// [EN] print out the results of the comparison
if (comparisonResult > 0) {
Localization.printLocalized(
String.format("Объем синей коробки (%,d куб. см) больше объёма красной (%,d куб. см).", blueBox.getVolume(), redBox.getVolume()),
String.format("The volume of the blue box (%,d cubic cm) is greater than the volume of the red one (%,d cubic cm).", blueBox.getVolume(), redBox.getVolume()));
} else if (comparisonResult == 0) {
Localization.printLocalized(
String.format("Синяя и красная коробки одного объема (%,d куб. см).", redBox.getVolume()),
String.format("Blue and red boxes of the same volume (%,d cubic cm).", redBox.getVolume()));
} else {
Localization.printLocalized(
String.format("Объем красной коробки (%,d куб. см) больше объёма синей (%,d куб. см).", redBox.getVolume(), blueBox.getVolume()),
String.format("The volume of the red box (%,d cubic cm) is greater than the volume of the blue one (%,d cubic cm).", redBox.getVolume(), blueBox.getVolume()));
}
}
}
Мы определили две подарочных коробки - создали экземпляры класса GiftBox с именами redBox ("красная коробка") и blueBox ("синяя коробка") и следующими характеристиками:
- redBox: название коробки=Red Box, ширина=100 см, высота=100 см, длина=100 см
- blueBox: название коробки=Blue Box, ширина=30 см, высота=60 см, длина=100 см
Затем мы видим строку кода, которая вызывает метод compareTo() интерфейса Comparable, который был переопределён в нашем классе GiftBox:
// [RU] сравниваем синюю коробку с красной
// [EN] comparing the blue box with the red one
int comparisonResult = blueBox.compareTo(redBox);
Мы помним, что результат сравнения может быть -1, 0 или 1 - если синяя коробка blueBox меньше, равна или больше красной коробки redBox, соответственно. Именно это значение получит переменная comparisonResult.
Теперь нам остаётся лишь проверить результат сравнения, который запишется в comparisonResult и вывести на экран консоли соответствующее сообщение. Метод printLocalized() класса Localization используется в примерах нашего сайта для поддержки локализации на русский и английский язык. Фактически он выполняет то же самое, что всем знакомый System.out.println, просто выведет одно из двух сообщений, в зависимости от настроек текущей локали для всех примеров с сайта:
// [RU] распечать результаты сравнения
// [EN] print out the results of the comparison
if (comparisonResult > 0) {
Localization.printLocalized(
String.format("Объем синей коробки (%,d куб. см) больше объёма красной (%,d куб. см).", blueBox.getVolume(), redBox.getVolume()),
String.format("The volume of the blue box (%,d cubic cm) is greater than the volume of the red one (%,d cubic cm).", blueBox.getVolume(), redBox.getVolume()));
} else if (comparisonResult == 0) {
Localization.printLocalized(
String.format("Синяя и красная коробки одного объема (%,d куб. см).", redBox.getVolume()),
String.format("Blue and red boxes of the same volume (%,d cubic cm).", redBox.getVolume()));
} else {
Localization.printLocalized(
String.format("Объем красной коробки (%,d куб. см) больше объёма синей (%,d куб. см).", redBox.getVolume(), blueBox.getVolume()),
String.format("The volume of the red box (%,d cubic cm) is greater than the volume of the blue one (%,d cubic cm).", redBox.getVolume(), blueBox.getVolume()));
}
Запустите программу, и увидите следующий результат на консоли:
Объем красной коробки (1 000 000 куб. см) больше объёма синей (180 000 куб. см).
Обращу внимание читателей ещё на один момент - в функции String.format() вы могли заметить выражение %,d - оно используется для удобного разделения разрядов чисел, в соответствии со специфичными настройками текущей локали в системе. Например, если объём коробки будет равен 1000000 (куб. см), то на консоль будет выведено 1 000 000, что упрощает восприятие и чтение длинных чисел.
Далее мы посмотрим ещё на одно интересное свойство интерфейса Comparable - это возможность использовать метод compareTo() для сортировки списков из объектов, реализующих интерфейс Comparable, по умолчанию, без явной передачи компаратора. Сделаем это на примере вызова метода sort() класса Collections из стандартного пакета java.util.
Для этого добавим ещё следующие строки в наш метод main() класса ComparableGiftBoxExample:
// [RU] Создаём ещё одну маленькую жёлтую коробку
// [EN] Create another small yellow box
GiftBox smallYellowBox = new GiftBox("Small Yellow Box", 10, 15, 30);
// [RU] Создаём список из наших экземпляров подарочных коробок
// [EN] Create a list containing our gift box instances
List<GiftBox> boxes = Arrays.asList(redBox, blueBox, smallYellowBox);
// [RU] Сортируем коробки в соответствии с их естественным порядком (natural ordering)
// [EN] Sort our boxes according to the natural ordering
Collections.sort(boxes);
// [RU] Выводим отсортированные коробки на консоль
// [EN] Print the sorted boxes to the console
Localization.printLocalized(
"Отсортированные коробки:",
"Sorted boxes:");
boxes.forEach(System.out::println);
Как видим, мы создали ещё один экземпляр подарочной коробки и затем поместили все три коробки в один список:
// [RU] Создаём список из наших экземпляров подарочных коробок
// [EN] Create a list containing our gift box instances
List<GiftBox> boxes = Arrays.asList(redBox, blueBox, smallYellowBox);
После чего выполнили сортировку нашего списка коробок:
// [RU] Сортируем коробки в соответствии с их естественным порядком (natural ordering)
// [EN] Sort our boxes according to the natural ordering
Collections.sort(boxes);
После чего мы выводим все наши коробки на экран при помощи метода forEach:
boxes.forEach(System.out::println);
Если вы сейчас запустите программу, то в конце вывода на консоли заметите следующее:
Отсортированные коробки:
GiftBox{name='Small Yellow Box', width=10, height=15, length=30}
GiftBox{name='BlueBox', width=30, height=60, length=100}
GiftBox{name='RedBox', width=100, height=100, length=100}
Как видим, мы довольно просто отсортировали список наших коробок по возрастанию объёма коробок, или по их размеру.
Напоследок давайте проверим, что будет, если попробовать сравнить объект какой-то коробки с пустой ссылкой, т.е. null. Как вы помните, помимо значений -1, 0 и 1 метод compareTo() может также выбросить исключение NullPointerException, если в метод была подана ссылка на пустой объект. Убедимся, что так и есть, для этого дополним наш код следующими строками:
GiftBox emptyBox = null;
// [RU] Здесь при вызове метода compareTo() произойдет выброс исключения NullPointerException
// [EN] Here we'll get a NullPointerException thrown when calling compareTo() method
if (smallYellowBox.compareTo(emptyBox) == 0) {
// [RU] В этот код мы никогда не попадём из-за NullPointerException...
// [EN] This code will never be executed due to NullPointerException...
Localization.printLocalized(
String.format("Объем двух коробок одинаковый: %s (%,d) и %s (%,d)",
smallYellowBox.getName(),
smallYellowBox.getVolume(),
emptyBox.getName(),
emptyBox.getVolume()),
String.format("The volume of these two boxes is equal: %s (%,d) и %s (%,d)",
smallYellowBox.getName(),
smallYellowBox.getVolume(),
emptyBox.getName(),
emptyBox.getVolume())
);
}
Попробуйте запустить программу с этими строками. Вы увидите, что код внутри условия if никогда не выполняется - вместо этого происходит выброс исключения NullPointerException из метода compareTo():
Exception in thread "main" java.lang.NullPointerException
at ru.allineed.samples.comparable.GiftBox.compareTo(GiftBox.java:119)
at ru.allineed.samples.comparable.ComparableGiftBoxExample.main(ComparableGiftBoxExample.java:78)
Можно видеть, что выброс исключения идёт из строки 119 нашего класса GiftBox. Это как раз та строка, где мы пытаемся вызвать метод getVolume() для объекта that, приходящем в качестве аргумента вызова compareTo():
@Override
public int compareTo(GiftBox that) {
int thatVolume = that.getVolume(); // строка 119 класса GiftBox
int thisVolume = getVolume();
if (thisVolume < thatVolume) {
return -1;
} else if (thisVolume > thatVolume) {
return 1;
}
return 0;
}
Из-за того, что that == null во время исполнения программы, происходит выброс исключения NullPointerException. Поэтому запомните, что при попытках передать в compareTo() пустую ссылку на объект, будет происходить аналогичная ситуация.
Внизу статьи вы найдете ссылку на полный исходный код рассмотренного в статье примера, ссылку на наш репозиторий с примерами программ на Java и инструкцию по запуску примеров на вашем компьютере.
Спасибо за внимание, пишите ваши мысли и отзывы в комментариях под статьёй.
Примеры из этой статьи: https://github.com/AllineedRu/JavaExamples/blob/main/allineed-core/src/main/java/ru/allineed/samples/comparable/ComparableGiftBoxExample.java