Sortowanie obiektów przez Comparator i Collator
- Kolekcje i strumienie
- poziom: średni
- aktualizacja: 16 sierpnia 2026
Comparator pozwala opisać porządek bez wpisywania Comparable na stałe do klasy danych — i dobrze, bo ten sam produkt raz układasz po nazwie, raz po cenie malejąco, a raz po dostępności. Komparatory łączysz przez thenComparing, odwracasz przez reversed i wpinasz zarówno w List.sort, jak i w strumień. Dla polskich napisów zwykłe String.compareTo odpada, bo porównuje kody Unicode. Collator zna zasady polskiego alfabetu, więc „Łódź” postawi między „Lublinem” a „Mławą”, a nie za literą Z — zobaczysz to w dwóch ostatnich linijkach wydruku.
import java.math.BigDecimal;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
/**
* Sortowanie obiektów przez Comparator i Collator — skryptyJAVA.pl
* Zapisz jako: SortowanieObiektow.java
*/
public class SortowanieObiektow {
/** Prosty typ danych; rekord nie narzuca jednego, globalnego sposobu sortowania. */
public record Produkt(String nazwa, String kategoria, BigDecimal cena, int stan) {
public Produkt {
if (stan < 0) { throw new IllegalArgumentException("Stan nie może być ujemny."); }
}
}
/**
* Buduje komparator zgodny z polskim alfabetem.
* SECONDARY rozróżnia polskie znaki, ale ignoruje wielkość liter.
*/
public static Comparator<String> polskiTekst() {
Collator collator = Collator.getInstance(Locale.forLanguageTag("pl-PL"));
collator.setStrength(Collator.SECONDARY);
return collator::compare;
}
/** Sortuje kopię listy i wypisuje ją, nie zmieniając danych wejściowych. */
private static void wypisz(String tytul, List<Produkt> produkty,
Comparator<Produkt> comparator) {
System.out.println("\n--- " + tytul + " ---");
produkty.stream().sorted(comparator).forEach(produkt ->
System.out.printf(Locale.forLanguageTag("pl-PL"),
"%-13s | %-11s | %8.2f zł | %3d szt.%n",
produkt.nazwa(), produkt.kategoria(), produkt.cena(), produkt.stan()));
}
// ── PRZYKŁADY ───────────────────────────────────────────
public static void main(String[] args) {
List<Produkt> produkty = List.of(
new Produkt("Żelazko", "AGD", new BigDecimal("189.90"), 8),
new Produkt("Ładowarka", "Elektronika", new BigDecimal("89.00"), 21),
new Produkt("Ćwiek", "Narzędzia", new BigDecimal("0.45"), 900),
new Produkt("Zegar", "Elektronika", new BigDecimal("129.00"), 12),
new Produkt("Ącki notes", "Papiernicze", new BigDecimal("14.50"), 35),
new Produkt("Śrubokręt", "Narzędzia", new BigDecimal("24.90"), 0),
new Produkt("Młotek", "Narzędzia", new BigDecimal("54.90"), 16),
new Produkt("Żarówka", "Elektronika", new BigDecimal("18.00"), 70));
Comparator<String> poPolsku = polskiTekst();
Comparator<Produkt> poNazwie = Comparator.comparing(Produkt::nazwa, poPolsku);
wypisz("Nazwa według polskiego alfabetu", produkty, poNazwie);
Comparator<Produkt> poKategoriiINazwie =
Comparator.comparing(Produkt::kategoria, poPolsku)
.thenComparing(Produkt::nazwa, poPolsku);
wypisz("Kategoria, potem nazwa", produkty, poKategoriiINazwie);
Comparator<Produkt> poCenieMalejaco =
Comparator.comparing(Produkt::cena, Comparator.reverseOrder())
.thenComparing(Produkt::nazwa, poPolsku);
wypisz("Cena malejąco, remisy po nazwie", produkty, poCenieMalejaco);
Comparator<Produkt> dostepneNajpierw =
Comparator.comparing((Produkt produkt) -> produkt.stan() == 0)
.thenComparingInt(Produkt::stan)
.thenComparing(Produkt::nazwa, poPolsku);
wypisz("Dostępne najpierw, potem stan", produkty, dostepneNajpierw);
// List.sort zmienia listę, dlatego tworzymy modyfikowalną kopię.
List<Produkt> kopia = new ArrayList<>(produkty);
kopia.sort(poNazwie.reversed());
System.out.println("\nNazwy od końca: "
+ kopia.stream().map(Produkt::nazwa).toList());
// Rekord przepuszcza null w nazwie, więc przed NPE musi bronić komparator.
List<Produkt> zBrakami = new ArrayList<>(produkty);
zBrakami.add(new Produkt(null, "Nieznana", new BigDecimal("9.90"), 3));
Comparator<Produkt> nazwaNullNaKoniec =
Comparator.comparing(Produkt::nazwa, Comparator.nullsLast(poPolsku));
System.out.println("\nNazwy, brak na końcu: " + zBrakami.stream()
.sorted(nazwaNullNaKoniec).map(Produkt::nazwa).toList());
List<String> miasta = List.of("Żory", "Zabrze", "Łódź", "Lublin", "Mława");
System.out.println("Naturalnie: " + miasta.stream().sorted().toList());
System.out.println("Po polsku: " + miasta.stream().sorted(poPolsku).toList());
}
}
Jak użyć tego skryptu
- Zapisz kod jako
SortowanieObiektow.java(pod inną nazwą kompilator się nie zgodzi — plik nosi nazwę klasy publicznej) i uruchom przezjava SortowanieObiektow.java. Porównaj zwłaszcza dwie ostatnie linie: porządek naturalny napisu i porządek polskiegoCollator. - Zastąp rekord
Produktwłasnym typem, a odwołania takie jakProdukt::nazwaodpowiednimi metodami dostępowymi. Komparator nie wymaga zmian w samej klasie danych. - Łącz kolejne kryteria przez
thenComparing,thenComparingIntlubthenComparingDouble. Kryterium po prawej jest używane tylko wtedy, gdy wcześniejsze porównanie dało remis. - Użyj
lista.sort(comparator), gdy chcesz zmienić istniejącą modyfikowalną listę, albolista.stream().sorted(comparator).toList(), gdy potrzebujesz nowego, niemodyfikowalnego wyniku.
Warto wiedzieć
- Do tekstu, który zobaczy użytkownik, nie używaj
String.compareTo— porządek kodów Unicode nie ma nic wspólnego z polskim alfabetem. Właściwe reguły dajeCollatorutworzony dla językapl-PL. Pamiętaj przy tym, żeCollatorjest modyfikowalny i nie jest bezpieczny wątkowo: twórz osobny egzemplarz na operację lub wątek, zamiast trzymać jeden współdzielony w statycznym polu. reversed()odwraca cały komparator zbudowany przed tym wywołaniem, a nie ostatni dołączony człon — na tym najłatwiej się przejechać. Jeśli malejąco ma iść tylko jedno pole, odwróć komparator tego pola przy dołączaniu:Comparator.comparing(Produkt::cena, Comparator.reverseOrder()).thenComparing(Produkt::nazwa, poPolsku)da cenę malejąco przy nazwach nadal rosnąco — dokładnie tak działapoCenieMalejacow przykładzie.- Rekord waliduje tylko
stan, więcnew Produkt("X", "Y", null, 1)przejdzie bez mrugnięcia okiem, a wywróci się dopiero przy sortowaniu komparatorempoCenieMalejaco: użyty tamComparator.reverseOrder()nie obsługujenull. Jeśli brak ceny jest dopuszczalny, użyjComparator.nullsLast(Comparator.reverseOrder()); jeśli nie, dopiszObjects.requireNonNull(cena, "cena")do konstruktora rekordu. PrzykładnazwaNullNaKoniecpokazuje analogiczną obsługę braku nazwy przezComparator.nullsLast(poPolsku). - Komparator musi być przechodni i najlepiej spójny z
equals. Nie buduj go przez odejmowanie, np.a.stan() - b.stan()— przy dużych wartościach przepełnienieintodwróci wynik; użyjcomparingIntalboInteger.compare. Miej też z tyłu głowy, żepolskiTekst()z siłąSECONDARYspójny zequalsnie jest:new TreeSet<>(polskiTekst())po dodaniu „Zegar”, „ZEGAR” i „zegar” zawiera jeden element, dwa znikają po cichu. Do zbiorów i map z kluczem tekstowym bierzHashSet/HashMap, aCollatorzostaw do sortowania.
Podobne skrypty
Stream API w praktyce: filtrowanie, grupowanie i podsumowania
Dziewięć gotowych sekcji Stream API na liście pracowników: filter i map, sortowanie z thenComparing, groupingBy z counting i summingDouble, statystyki, Optional, joining i partitioningBy.
średni Zobacz kod → Kolekcje i strumienieNajczęstsze operacje na List, Map i Set
Operacje, które robisz na kolekcjach codziennie: dodawanie i usuwanie elementów listy, kasowanie duplikatów, część wspólna i różnica zbiorów oraz zliczanie i grupowanie danych w mapie.
początkujący Zobacz kod → Kolekcje i strumienieRecord, equals/hashCode i niemodyfikowalne kolekcje
Rekordy z walidacją w konstruktorze kompaktowym, automatyczne equals i hashCode w roli elementu zbioru i klucza mapy, obronna kopia List.copyOf oraz różnica między migawką a widokiem Collections.unmodifiableList.
średni Zobacz kod →