1. Проблема порівняння об’єктів
Нагадування: порівняння посилань і порівняння об’єктів
Ви вже знаєте, що в Java оператор == під час роботи з об’єктами порівнює їхні посилання — тобто чи зберігаються вони за однією й тією самою адресою в пам’яті. Два об’єкти з однаковими полями, але створені через new, будуть різними для ==.
Person p1 = new Person("Олександр", 20);
Person p2 = new Person("Олександр", 20);
System.out.println(p1 == p2); // false — це різні об’єкти в пам’яті.
А якщо нам потрібно з’ясувати, чи рівні вони за вмістом, використовуємо equals(), hashCode()… — ви це вже знаєте. А як визначити, хто «старший», «молодший» або що йде «раніше за абеткою»? Наприклад, щоб відсортувати список користувачів за віком або ім’ям.
Необхідність сортування й пошуку об’єктів
Припустімо, у нас є список користувачів, і ми хочемо відсортувати їх за віком:
List<Person> people = new ArrayList<>();
people.add(new Person("Василь", 25));
people.add(new Person("Петро", 20));
people.add(new Person("Катерина", 30));
// Як відсортувати?
Collections.sort(people); // Java не знає, як порівнювати Person.
Компілятор одразу повідомить: клас Person не реалізує інтерфейс Comparable. Java не читає думок і не знає, що саме для нас означає «більше» або «менше» для Person. Щоб навчити її цього, потрібно явно описати правила порівняння.
2. Інтерфейс Comparable
Оголошення інтерфейсу
Інтерфейс Comparable — це стандартний спосіб повідомити Java: «Мій клас можна порівнювати, і ось як це робити».
public interface Comparable<T> {
int compareTo(T o);
}
Метод a.compareTo(b) повертає:
- від’ємне число — означає, що a «менше» за b.
- Якщо 0 — об’єкти вважаються рівними.
- додатне число — a «більше» за b.
Приклад: реалізація compareTo для класу Person
Створімо клас Person, який порівнюється за віком:
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Гетери (для прикладів далі)
public String getName() { return name; }
public int getAge() { return age; }
// Реалізація методу compareTo
@Override
public int compareTo(Person other) {
// Сортування за віком (за зростанням)
return Integer.compare(this.age, other.age);
// Альтернатива: return this.age - other.age;
}
}
Важливий момент: якщо ви хочете сортувати за спаданням, просто поміняйте місцями аргументи: Integer.compare(other.age, this.age).
Аналогія: compareTo — це як суддя на змаганні: він має чітко вирішити, хто попереду, хто позаду, а хто на одному рівні. Якщо всі судді (методи compareTo) судитимуть по-різному — почнеться хаос.
3. Використання Comparable
Сортування колекцій з Comparable
Тепер, коли наш клас реалізує Comparable, сортування працює відразу:
List<Person> people = new ArrayList<>();
people.add(new Person("Василь", 25));
people.add(new Person("Петро", 20));
people.add(new Person("Катерина", 30));
Collections.sort(people); // Використовує compareTo!
for (Person p : people) {
System.out.println(p.getName() + " (" + p.getAge() + ")");
}
// Петро (20)
// Василь (25)
// Катерина (30)
Аналогічно працює й метод sort у списку:
people.sort(null); // Якщо передати null, використовується compareTo
Сортування за ім’ям
Якщо хочемо сортувати за ім’ям — змінюємо реалізацію:
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
Сортування за кількома полями
Іноді потрібно порівнювати спочатку за одним полем, а за рівності — за іншим:
@Override
public int compareTo(Person other) {
int cmp = Integer.compare(this.age, other.age);
if (cmp != 0) return cmp;
return this.name.compareTo(other.name);
}
4. Найкращі практики під час реалізації Comparable
Дотримуйтеся контракту Comparable
- Якщо a.compareTo(b) == 0, то b.compareTo(a) обов’язково має бути 0.
- Якщо a.compareTo(b) < 0, то b.compareTo(a) має бути > 0 (і навпаки).
- Якщо a.compareTo(b) == 0, бажано, щоб a.equals(b) повертало true (але це не суворо обов’язково).
Чому це важливо?
Колекції (наприклад, TreeSet, TreeMap) і методи сортування можуть поводитися непередбачувано, якщо контракт порушено. Наприклад, у колекції, де цього не має бути, можуть з’явитися «дублікати».
Не забувайте про equals і hashCode
Якщо ви реалізуєте compareTo, подумайте, чи правильно реалізовані equals і hashCode. Особливо якщо ваш клас використовуватиметься у колекціях типу HashSet або Map.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person other = (Person) o;
return age == other.age && Objects.equals(name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
Не використовуйте у методі compareTo поля, які можуть бути null, без перевірки
Якщо поле може бути null, використовуйте безпечні порівняння:
@Override
public int compareTo(Person other) {
return Objects.compare(this.name, other.name, Comparator.nullsFirst(String::compareTo));
}
Не змінюйте поля, що беруть участь у compareTo, якщо об’єкт вже перебуває у відсортованій колекції
Це може призвести до того, що об’єкт «загубиться» всередині колекції — наприклад, у TreeSet або TreeMap.
5. Розвиваємо навчальний застосунок: сортування користувачів
Крок 1: Описуємо клас
public class Person implements Comparable<Person> {
private String name;
private int age;
// ... конструктор, гетери, compareTo, equals, hashCode ...
}
Крок 2: Додаємо користувачів
List<Person> people = new ArrayList<>();
people.add(new Person("Марія", 23));
people.add(new Person("Григорій", 19));
people.add(new Person("Ганна", 25));
Крок 3: Сортуємо та друкуємо
Collections.sort(people);
for (Person p : people) {
System.out.println(p.getName() + " (" + p.getAge() + ")");
}
Результат:
Григорій (19)
Марія (23)
Ганна (25)
6. Схема роботи Comparable
┌────────────────────────────┐
│ Ваш клас (Person) │
├────────────────────────────┤
│ implements Comparable │
│ ↓ │
│ public int compareTo(T o) │
│ ↓ │
│ (this < o) → -1 │
│ (this == o) → 0 │
│ (this > o) → 1 │
└────────────────────────────┘
│
▼
Collections.sort(list)
│
▼
Сортування працює!
7. Корисні нюанси
Як працює Collections.sort
- Якщо список містить об’єкти, що реалізують Comparable, сортування використовуватиме їхній метод compareTo.
- Інакше отримаєте помилку компіляції.
- Для стандартних типів (Integer, String тощо) Comparable уже реалізовано.
Чи можна мати кілька способів порівняння?
- В одному класі — лише один «природний порядок», визначений через Comparable.
- Для альтернативних порядків використовуйте Comparator (наступна лекція).
Приклад: compareTo для рядків
String a = "apple";
String b = "banana";
System.out.println(a.compareTo(b)); // від’ємне число, тому що "apple" < "banana"
Таблиця: що повертає compareTo
| Порівняння | Повернене значення |
|---|---|
|
|
|
|
|
|
8. Типові помилки під час реалізації Comparable
Помилка № 1: Порушення контракту compareTo.
Якщо a.compareTo(b) повертає 0, а b.compareTo(a) — не 0, колекції поводитимуться дивно. Наприклад, TreeSet може вважати об’єкти різними й додати обидва.
Помилка № 2: Використання неініціалізованих (null) полів.
Якщо поле, за яким порівнюєте, може бути null, а ви не робите перевірку — отримаєте NullPointerException.
Помилка № 3: Неузгодженість compareTo та equals.
Якщо compareTo каже, що об’єкти рівні (0), а equals — що різні (false), це призведе до помилок під час роботи з колекціями.
Помилка № 4: Зміна полів, які беруть участь у compareTo, після додавання до відсортованої колекції.
Це як змінити прізвище в паспорті, коли ви вже стоїте в черзі за абеткою. Колекція може «загубити» ваш об’єкт.
Помилка № 5: Повертати лише -1, 0 або 1.
Метод compareTo може повертати будь-яке від’ємне або додатне число, не обов’язково саме -1 чи 1. Але для простоти часто використовують -1/0/1.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ