CodeGym /Kursy /JAVA 25 SELF /Interfejs Comparator: tworzenie i użycie

Interfejs Comparator: tworzenie i użycie

JAVA 25 SELF
Poziom 29, Lekcja 3
Dostępny

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
int compareTo(T o)
int compare(T o1, T o2)
Ile wariantów? Tylko jeden „naturalny” Dowolnie wiele, do różnych potrzeb
Zastosowanie
Collections.sort(list)
Collections.sort(list, comp)
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.

1
Zadanie
JAVA 25 SELF, poziom 29, lekcja 3
Niedostępne
Sortowanie listy uczestników według wieku na wydarzeniu 🧑‍🎓
Sortowanie listy uczestników według wieku na wydarzeniu 🧑‍🎓
1
Zadanie
JAVA 25 SELF, poziom 29, lekcja 3
Niedostępne
Sortowanie uczestników według długości imienia dla identyfikatorów 🏷️
Sortowanie uczestników według długości imienia dla identyfikatorów 🏷️
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION