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ść |
|---|---|
|
|
|
|
|
|
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.
GO TO FULL VERSION