JavaRush /Курси /JAVA 25 SELF /Інтерфейс Comparable: реалізація та compareTo

Інтерфейс Comparable: реалізація та compareTo

JAVA 25 SELF
Рівень 29 , Лекція 2
Відкрита

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

Порівняння Повернене значення
this < o
< 0
this == o
0
this > o
> 0

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.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ