CodeGym /Kursy /JAVA 25 SELF /Interfejs Comparable: implementacja, compareTo

Interfejs Comparable: implementacja, compareTo

JAVA 25 SELF
Poziom 29 , Lekcja 2
Dostępny

1. Problem porównywania obiektów

Przypomnienie: porównywanie referencji a porównywanie obiektów

Wiesz już, że w Javie operator == przy pracy z obiektami porównuje ich referencje – czyli to, czy znajdują się pod tym samym adresem w pamięci. Dwa obiekty z identycznymi polami, ale utworzone przez new, będą różne dla ==.

Person p1 = new Person("Sasha", 20);
Person p2 = new Person("Sasha", 20);

System.out.println(p1 == p2); // false – to różne obiekty w pamięci!

A jeśli chcemy sprawdzić, czy są równe co do zawartości, używamy equals(), hashCode() ... no, już to znasz. A co, jeśli musimy rozstrzygnąć, kto jest „starszy”, „młodszy”, „wyżej w alfabecie”? Na przykład, aby posortować listę użytkowników po wieku albo po imieniu.

Konieczność sortowania i wyszukiwania obiektów

Załóżmy, że mamy listę użytkowników i chcemy posortować ich po wieku:

List<Person> people = new ArrayList<>();
people.add(new Person("Vasya", 25));
people.add(new Person("Petya", 20));
people.add(new Person("Katya", 30));

// Jak posortować?
Collections.sort(people); // Ups! A Java nie wie, jak porównywać Person!

Kompilator natychmiast zgłosi błąd: klasa Person nie implementuje interfejsu Comparable. Java nie czyta w myślach i nie wie, co dla nas oznacza „większy” lub „mniejszy” dla Person. Aby ją tego nauczyć, musimy jawnie opisać zasady porównywania.

2. Interfejs Comparable

Deklaracja interfejsu

Interfejs Comparable to standardowy sposób, by powiedzieć Javie: „Moja klasa jest porównywalna i oto, jak to robić”.

public interface Comparable<T> {
    int compareTo(T o);
}

Nasz stary znajomy a.compareTo(b) zwróci:

  • liczbę ujemną – czyli a jest „mniejsze” od b.
  • Jeśli 0 – obiekty są uznane za równe.
  • liczbę dodatniąa jest „większe” od b.

Przykład: implementacja compareTo dla klasy Person

Stwórzmy klasę Person, którą można porównywać po wieku:

public class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Gettery (na potrzeby dalszych przykładów)
    public String getName() { return name; }
    public int getAge() { return age; }

    // Implementacja metody compareTo
    @Override
    public int compareTo(Person other) {
        // Sortowanie po wieku (rosnąco)
        return Integer.compare(this.age, other.age);
        // Alternatywa: return this.age - other.age;
    }
}

Ważny punkt: jeśli chcesz sortować malejąco, po prostu zamień argumenty miejscami: Integer.compare(other.age, this.age).

Analogia. compareTo jest jak sędzia na zawodach: musi jasno rozstrzygnąć, kto jest przed kim, a kto na tym samym poziomie. Jeśli wszyscy sędziowie (metody compareTo) będą orzekać inaczej – zapanuje chaos!

3. Użycie Comparable

Sortowanie kolekcji za pomocą Comparable

Teraz, gdy nasza klasa implementuje Comparable, sortowanie działa „od razu”:

List<Person> people = new ArrayList<>();
people.add(new Person("Vasya", 25));
people.add(new Person("Petya", 20));
people.add(new Person("Katya", 30));

Collections.sort(people); // Używa compareTo!

for (Person p : people) {
    System.out.println(p.getName() + " (" + p.getAge() + ")");
}
// Petya (20)
// Vasya (25)
// Katya (30)

Analogicznie działa metoda sort na liście:

people.sort(null); // Jeśli przekażesz null, użyte zostanie compareTo

Sortowanie po imieniu

Jeśli chcemy sortować po imieniu – zmieniamy implementację:

@Override
public int compareTo(Person other) {
    return this.name.compareTo(other.name);
}

Sortowanie po kilku polach

Czasem trzeba porównywać najpierw po jednym polu, a przy równości – po innym:

@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. Dobre praktyki przy implementacji Comparable

Przestrzegaj kontraktu Comparable

  • Jeśli a.compareTo(b) == 0, to b.compareTo(a) koniecznie musi być 0.
  • Jeśli a.compareTo(b) < 0, to b.compareTo(a) powinno być > 0 (i odwrotnie).
  • Jeśli a.compareTo(b) == 0, pożądane jest, aby a.equals(b) było true (choć nie jest to bezwzględnie wymagane).

Dlaczego to ważne?
Kolekcje (na przykład TreeSet, TreeMap) i metody sortujące mogą zachowywać się nieprzewidywalnie, jeśli kontrakt jest naruszony. Na przykład mogą pojawić się duplikaty w kolekcji, w której nie powinno ich być.

Nie zapominaj o equals i hashCode

Jeśli implementujesz compareTo, zastanów się: czy poprawnie zaimplementowano equals i hashCode? Zwłaszcza jeśli Twoja klasa będzie używana w kolekcjach typu HashSet lub 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);
}

Nie używaj w compareTo pól, które mogą być null, bez sprawdzenia!

Jeśli pole może mieć wartość null, użyj bezpiecznego porównania:

@Override
public int compareTo(Person other) {
    return Objects.compare(this.name, other.name, Comparator.nullsFirst(String::compareTo));
}

Nie zmieniaj pól uwzględnianych w compareTo, jeśli obiekt znajduje się już w posortowanej kolekcji

Może to doprowadzić do „zagubienia się” obiektu wewnątrz kolekcji – na przykład w TreeSet lub TreeMap.

5. Rozwijamy aplikację edukacyjną: sortowanie użytkowników

Krok 1: Opisujemy klasę

public class Person implements Comparable<Person> {
    private String name;
    private int age;

    // ... konstruktor, gettery, compareTo, equals, hashCode ...
}

Krok 2: Dodajemy użytkowników

List<Person> people = new ArrayList<>();
people.add(new Person("Masha", 23));
people.add(new Person("Grisha", 19));
people.add(new Person("Anya", 25));

Krok 3: Sortujemy i wypisujemy

Collections.sort(people);

for (Person p : people) {
    System.out.println(p.getName() + " (" + p.getAge() + ")");
}

Wynik:

Grisha (19)
Masha (23)
Anya (25)

6. Schemat działania Comparable

┌────────────────────────────┐
│   Twoja klasa (Person)     │
├────────────────────────────┤
│ implements Comparable      │
│   ↓                        │
│ public int compareTo(T o)  │
│   ↓                        │
│ (this < o) → -1            │
│ (this == o) → 0            │
│ (this > o) → 1             │
└────────────────────────────┘
        │
        ▼
Collections.sort(list)
        │
        ▼
   Sortowanie działa!

7. Przydatne szczegóły

Jak działa Collections.sort

  • Jeśli lista zawiera obiekty implementujące Comparable, sortowanie użyje ich metody compareTo.
  • Jeśli nie implementuje – będzie błąd kompilacji.
  • Dla typów standardowych (Integer, String itd.) Comparable jest już zaimplementowany.

Czy można mieć kilka sposobów porównywania?

  • W jednej klasie – tylko jeden „naturalny porządek” poprzez Comparable.
  • Dla alternatywnych porządków użyj Comparator (następna lekcja).

Przykład: compareTo dla napisów

String a = "apple";
String b = "banana";
System.out.println(a.compareTo(b)); // liczba ujemna, ponieważ "apple" < "banana"

Tabela: co zwraca compareTo

Porównanie Zwracana wartość
this < o
< 0
this == o
0
this > o
> 0

8. Typowe błędy przy implementacji Comparable

Błąd nr 1: Naruszenie kontraktu compareTo.
Jeśli a.compareTo(b) zwraca 0, a b.compareTo(a) – nie 0, kolekcje będą zachowywać się dziwnie. Na przykład TreeSet może uznać obiekty za różne i dodać oba.

Błąd nr 2: Używanie niezainicjalizowanych pól (null).
Jeśli pole, po którym porównujesz, może być null, a nie wykonasz sprawdzenia – dostaniesz NullPointerException.

Błąd nr 3: Niespójność compareTo i equals.
Jeśli compareTo mówi, że obiekty są równe (0), a equals – że różne (false), doprowadzi to do błędów przy pracy z kolekcjami.

Błąd nr 4: Zmiana pól uwzględnianych w compareTo po dodaniu do posortowanej kolekcji.
To jak zmiana nazwiska w paszporcie, gdy już stoisz w kolejce alfabetycznej. Kolekcja może „zgubić” Twój obiekt.

Błąd nr 5: Zwracanie tylko -1, 0 lub 1.
Metoda compareTo może zwracać dowolną liczbę ujemną lub dodatnią, niekoniecznie dokładnie -1 czy 1. Ale dla prostoty często używa się -1/0/1.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION