1. Wprowadzenie
W praktyce rzadko wystarcza jeden sposób porównywania obiektów. Wyobraź sobie, że masz listę użytkowników: czasem chcesz sortować ich po imieniu, czasem – po wieku, a innym razem – po długości nazwiska. Albo masz klasę, której nie jesteś autorem, więc nie możesz dodać do niej compareTo. Właśnie na takie sytuacje w Javie istnieje interfejs Comparator.
Kiedy Comparable nie wystarcza
- Klasy nie da się zmienić (np. pochodzi z zewnętrznej biblioteki).
- Potrzebujesz kilku sposobów sortowania (po różnych polach).
- Chcesz oddzielić logikę porównywania od samej klasy (np. sortować różnie w różnych częściach programu).
Analogia
Jeśli Comparable to wbudowany „naturalny porządek” obiektu, to Comparator to zewnętrzny sędzia, który może oceniać twoje obiekty według dowolnych kryteriów: dziś po imieniu, jutro – po wieku, pojutrze – po długości imienia.
2. Interfejs Comparator: składnia i kontrakt
Deklaracja interfejsu
public interface Comparator<T> {
int compare(T o1, T o2);
}
Metoda compare powinna zwracać:
- Liczbę ujemną, jeśli pierwszy obiekt jest „mniejszy” od drugiego.
- 0, jeśli są równe.
- Liczbę dodatnią, jeśli pierwszy jest „większy” od drugiego.
Kontrakt jest taki sam jak w Comparable, tylko teraz porównywane są dwa obiekty, a nie „bieżący” i „inny” za pomocą compareTo.
Przykład: komparator do sortowania po nazwisku
Załóżmy, że mamy klasę Person:
public class Person {
private String firstName;
private String lastName;
private int age;
// Konstruktor i gettery
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
}
Utwórzmy komparator sortujący po nazwisku:
import java.util.Comparator;
public class LastNameComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return a.getLastName().compareTo(b.getLastName());
}
}
Uwaga: metoda compareTo w klasie String porównuje je w porządku alfabetycznym.
3. Użycie Comparator: sortowanie kolekcji
Sortowanie z użyciem komparatora
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Anna", "Kostetska", 25));
people.add(new Person("Borys", "Novak", 20));
people.add(new Person("Wiktoria", "Bell", 22));
// Sortowanie po nazwisku
Collections.sort(people, new LastNameComparator());
for (Person p : people) {
System.out.println(p.getLastName() + " " + p.getFirstName());
}
}
}
Wynik:
Novak Borys
Kostetska Anna
Bell Wiktoria
Sortowanie według wieku za pomocą komparatora
Nawet jeśli klasa już implementuje Comparable według imienia, można utworzyć osobny komparator – według wieku:
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return Integer.compare(a.getAge(), b.getAge());
}
}
I użyć analogicznie:
Collections.sort(people, new AgeComparator());
Wynik:
Borys Novak (20)
Wiktoria Bell (22)
Anna Kostetska (25)
Przykład: wybór komparatora „w locie”
Collections.sort(people, new LastNameComparator()); // Po nazwisku
Collections.sort(people, new AgeComparator()); // Według wieku
4. Klasy anonimowe i wyrażenia lambda
Komparatory można tworzyć „w locie”, bez deklarowania osobnych klas.
Klasa anonimowa
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getFirstName().compareTo(b.getFirstName());
}
});
Wyrażenie lambda
Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));
Albo jeszcze krócej metodą listy List.sort:
people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
- Klasy anonimowe – stary sposób, dość rozwlekły.
- Lambda – nowocześnie i zwięźle.
5. Przykłady: sortowanie według różnych kryteriów
Sortowanie według długości nazwiska
Comparator<Person> byLastNameLength = (a, b) ->
Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);
Sortowanie według wieku, a następnie według imienia (wielopoziomowe)
Comparator<Person> byAgeThenName = (a, b) -> {
int cmp = Integer.compare(a.getAge(), b.getAge());
if (cmp != 0) return cmp;
return a.getFirstName().compareTo(b.getFirstName());
};
people.sort(byAgeThenName);
Użycie komparatora do wyszukiwania (przykład)
Komparator przydaje się nie tylko do sortowania, ale także do wyszukiwania w posortowanych kolekcjach:
// people muszą być posortowani według wieku!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
System.out.println("Znaleziono osobę w wieku 22 lat: " + people.get(idx));
}
6. Dobre praktyki i cechy pracy z Comparator
Nie naruszaj kontraktu
- Jeśli compare(a, b) zwraca 0, to compare(b, a) również powinno zwrócić 0.
- Jeśli compare(a, b) > 0, to compare(b, a) < 0.
- Uwzględniaj możliwe wartości null (zob. niżej).
Nie zapominaj o equals i hashCode
Mimo że komparatory porównują obiekty „na swój sposób”, dla struktury w rodzaju TreeSet lub przy wyszukiwaniu kluczy w TreeMap ważne jest, aby logika porównywania według komparatora była zgodna z equals. W przeciwnym razie można uzyskać nieoczekiwane rezultaty: dwa różne obiekty są uznane za równe przez komparator, ale nie są równe według equals.
Sortowanie z uwzględnieniem null
Jeśli pola mogą być null, użyj „gotowych” pomocników:
Comparator<Person> byLastNameNullSafe = Comparator.comparing(
Person::getLastName,
Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);
7. Przydatne niuanse
Tabela: porównanie Comparable i Comparator
| Comparable | Comparator | |
|---|---|---|
| Gdzie jest implementowany? | W samej klasie | W osobnej klasie/lambdzie |
| Metoda | |
|
| Ile wariantów? | Tylko jeden „naturalny” | Dowolnie wiele, do różnych potrzeb |
| Zastosowanie | |
|
| Działa dla cudzych klas? | Nie | Tak |
Przykład: sortowanie malejąco
Porządek można odwrócić ręcznie:
Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);
Albo za pomocą reversed():
Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());
8. Typowe błędy przy pracy z Comparator
Błąd nr 1: naruszenie kontraktu porównywania. Jeśli zapominasz, że compare(a, b) i compare(b, a) powinny mieć przeciwne znaki, albo zwracasz dowolne wartości (np. samą różnicę — a.getAge() - b.getAge(), co może prowadzić do przepełnienia), wynik będzie nieprzewidywalny. Używaj Integer.compare, a nie odejmowania — to bezpieczniejsze.
Błąd nr 2: ignorowanie wartości null. Jeśli pola, które porównujesz, mogą być null, koniecznie obsłuż ten przypadek (np. przez Comparator.nullsFirst/Comparator.nullsLast), w przeciwnym razie łatwo o NullPointerException w najmniej oczekiwanym momencie.
Błąd nr 3: niestabilne kryteria sortowania. Jeśli komparator zwraca różne wartości dla tych samych obiektów (np. używa liczby losowej lub silnie zmiennego pola), sortowanie może zachowywać się chaotycznie.
Błąd nr 4: niespójność z equals. Jeśli compare(a, b) == 0, ale a.equals(b) zwraca false, kolekcje takie jak TreeSet i TreeMap mogą działać inaczej, niż oczekujesz. Najlepiej, aby równość według komparatora i według equals była zgodna.
Błąd nr 5: sortowanie bez komparatora dla cudzych klas. Jeśli próbujesz sortować obiekty „cudzej” klasy bez Comparable i bez przekazania Comparator, otrzymasz błąd kompilacji. Przekazuj jawny komparator.
GO TO FULL VERSION