Der Comparator begegnet dir in Java spätestens dann, wenn du anfängst, Listen nicht nur "irgendwie", sondern gezielt nach bestimmten Eigenschaften sortieren zu wollen. Vielleicht willst du Modelle nach einem Feld ordnen, Daten fürs Frontend vorbereiten oder einfach nur eine Liste von Objekten sauber und strukturiert ausgeben. Genau hier spielt der Comparator seine Stärken aus: Du kannst Vergleichslogik definieren, ohne deine Klassen selbst anzupassen, und behältst dabei volle Kontrolle über das Sortierverhalten.
In diesem Artikel schauen wir uns an, wie ein Comparator funktioniert, warum er so flexibel ist und wie du ihn praktisch einsetzt - von einfachen Beispielen bis hin zu sauber lesbaren Comparator-Ketten. Ziel ist, dass du danach Sortierungen schreiben kannst, die genau das tun, was du brauchst, ohne Umwege oder Verwirrung.
Comparator vs. Comparable - was ist was?
In Java gibt es zwei zentrale Konzepte, wenn es ums Vergleichen von Objekten geht: das Interface Comparable und das Interface Comparator.
Comparable baust du direkt in deine Klasse ein. Du sagst damit: "Objekte dieser Klasse wissen selbst, wie sie sich untereinander vergleichen sollen." Das machst du, indem deine Klasse Comparable<T> implementiert und die Methode compareTo überschreibt.
Comparator ist externer. Du sagst damit: "Ich habe eine separate Vergleichslogik, die zwei Objekte nimmt und entscheidet, welches 'größer', 'kleiner' oder gleich ist." Du musst die Klasse selbst nicht anfassen. Das ist besonders praktisch, wenn du dieselbe Klasse in verschiedenen Sortierreihenfolgen brauchst.
Der Kern von Comparator ist eine einzige Methode:
int compare(T a, T b);
Die Rueckgabewerte bedeuten immer dasselbe:
- < 0: a ist "kleiner" als b
- 0: a und b gelten als gleich
- > 0: a ist "groesser" als b
Das war's. Darauf baut alles auf – egal wie komplex deine Sortierung wird.
Ein einfaches Beispiel: Strings sortieren
Starten wir mit etwas Vertrautem: einer Liste von Strings. Standardmäßig kannst du Strings einfach mit Collections.sort sortieren, weil String selbst Comparable implementiert.
Aber nehmen wir an, du willst nicht alphabetisch sortieren, sondern nach Länge des Strings. Genau hier kommt Comparator ins Spiel:
List<String> namen = new ArrayList<>();
namen.add("Anna");
namen.add("Maximilian");
namen.add("Tom");
namen.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
// Laengere Strings sollen nach hinten
return Integer.compare(a.length(), b.length());
}
});
Wichtige Punkte in diesem Beispiel:
- Wir übergeben einen Comparator<String> an sort.
- In compare vergleichen wir die Länge der beiden Strings.
- Integer.compare hilft uns, nicht selbst mit if-Kaskaden zu arbeiten.
Seit Java 8 geht das noch kürzer, dazu gleich mehr.
Comparator mit eigenen Klassen nutzen
Spannend wird es, wenn du eigene Klassen sortieren willst. Stell dir eine einfache Person-Klasse vor:
public class Person {
private String name;
private int alter;
public Person(String name, int alter) {
this.name = name;
this.alter = alter;
}
public String getName() {
return name;
}
public int getAlter() {
return alter;
}
}
Jetzt hast du eine Liste von Personen und willst nach Alter sortieren:
List<Person> personen = new ArrayList<>();
personen.add(new Person("Anna", 28));
personen.add(new Person("Max", 35));
personen.add(new Person("Tom", 22));
personen.sort(new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return Integer.compare(a.getAlter(), b.getAlter());
}
});
Damit hast du eine aufsteigende Sortierung nach Alter. Wenn du absteigend sortieren willst, kannst du einfach die Parameter tauschen:
public int compare(Person a, Person b) {
return Integer.compare(b.getAlter(), a.getAlter());
}
Oder du verwendest später die Hilfsmethode reversed().
Comparator mit Lambdas und Hilfsmethoden
Seit Java 8 ist Comparator deutlich angenehmer zu verwenden. Du musst nicht jedes Mal eine anonyme Klasse schreiben. Stattdessen kannst du Lambdas und statische Methoden aus Comparator nutzen.
Dasselbe Beispiel mit Person sieht dann so aus:
personen.sort((a, b) -> Integer.compare(a.getAlter(), b.getAlter()));
Noch lesbarer wird es mit Comparator.comparing:
personen.sort(
Comparator.comparing(Person::getAlter)
);
Wenn du absteigend sortieren willst:
personen.sort(
Comparator.comparing(Person::getAlter).reversed()
);
Du kannst auch nach Strings sortieren, zum Beispiel nach dem Namen:
personen.sort(
Comparator.comparing(Person::getName)
);
Oder die Sortierung kombinieren: erst nach Alter, dann nach Name. Das ist praktisch, wenn mehrere Personen gleich alt sind:
personen.sort(
Comparator.comparing(Person::getAlter)
.thenComparing(Person::getName)
);
Die Kette liest sich fast wie ein Satz: "Sortiere nach Alter, und wenn das gleich ist, dann nach Name."
Comparator oder Comparable - was soll ich nehmen?
Gute Frage, die sich in der Praxis haeufig stellt. Grob gesagt:
- Nutze Comparable, wenn deine Klasse eine "natürliche" Sortierung hat, die fast immer Sinn ergibt. Beispiel: Zahlen aufsteigend, Datumswerte chronologisch, Strings alphabetisch.
- Nutze Comparator, wenn du mehrere unterschiedliche Sortierreihenfolgen für dieselbe Klasse brauchst oder die Klasse selbst nicht ändern kannst (z.B. Fremd-Bibliothek).
Du kannst beides kombinieren. Eine Klasse kann Comparable implementieren und du kannst trotzdem weitere Comparatoren bauen, wenn du sie brauchst.
Typische Stolperfallen mit Comparator
Ein paar Dinge solltest du im Hinterkopf behalten, damit dir die gängigen Fehler erspart bleiben.
1. Niemals direkt Zahlen subtrahieren
Viele schreiben am Anfang so etwas:
public int compare(Person a, Person b) {
return a.getAlter() - b.getAlter();
}
Das funktioniert in einfachen Beispielen oft, ist aber unsauber. Bei großen Zahlen kann es zu Integer-Overflow kommen, und bei anderen Datentypen passt das nicht. Sauberer ist:
public int compare(Person a, Person b) {
return Integer.compare(a.getAlter(), b.getAlter());
}
Oder gleich Comparator.comparing(Person::getAlter).
2. Konsistente Logik
Dein Comparator sollte immer konsistent sein: Wenn compare(a, b) 0 zurückgibt, dann sollten a und b logisch als gleich behandelt werden. Wenn du da zu kreativ wirst, kann die Sortierung unvorhersehbar werden.
3. Null-Werte
Wenn in deinen Daten Felder null sein können, musst du das abfangen. Sonst fliegt dir bei sort eine NullPointerException um die Ohren.
Dafür gibt es z.B. Comparator.nullsFirst und Comparator.nullsLast:
Comparator<Person> nachNameMitNulls = Comparator.comparing(
Person::getName,
Comparator.nullsLast(String::compareTo)
);
Damit landen Personen ohne Namen am Ende der Sortierung. Wenn du komplett vermeiden willst, dass nulls vorkommen, ist es natürlich besser, die Daten vorher aufzuräumen. Aber für reale Systeme ist null-Unterstützung oft nötig.
Comparator wiederverwenden statt duplizieren
Ein guter Stil ist, häufig verwendete Comparatoren an einer zentralen Stelle zu definieren. Zum Beispiel als public static final Konstanten in deiner Klasse:
public class Person {
// ... Felder, Konstruktor, Getter ...
public static final Comparator<Person> NACH_NAME =
Comparator.comparing(Person::getName);
public static final Comparator<Person> NACH_ALTER =
Comparator.comparing(Person::getAlter);
}
Dann kannst du an anderer Stelle einfach schreiben:
personen.sort(Person.NACH_NAME);
Das macht deinen Code lesbarer und vermeidet Copy & Paste von Vergleichslogik. Und wenn du später etwas ändern musst, tust du das an genau einer Stelle.
Sortieren mit Streams und Comparator
Wenn du schon mit Streams arbeitest, begegnet dir Comparator dort auch. Beispiel: Du willst eine sortierte Liste zurückgeben, ohne die originale Liste zu verändern:
List<Person> sortiertNachAlter = personen.stream()
.sorted(Comparator.comparing(Person::getAlter))
.toList();
sorted verwendet hier intern genau denselben Comparator-Mechanismus. Der Vorteil: Du kannst fluenter mit Daten arbeiten, ohne Zwischenschritte auf der Original-Liste.
Wann brauchst du keinen eigenen Comparator?
Ein Punkt, der gerne vergessen wird: Oft brauchst du überhaupt keinen eigenen Comparator, weil der Datentyp schon eine natürliche Sortierung hat.
Beispiele:
- String sortiert alphabetisch.
- Integer, Long, Double sortieren numerisch aufsteigend.
- LocalDate, LocalDateTime, Instant sortieren chronologisch.
In diesen Faellen reicht oft:
List<Integer> zahlen = List.of(5, 1, 9, 3);
List<Integer> sortiert = zahlen.stream()
.sorted()
.toList();
Erst wenn du von dieser natürlichen Sortierung abweichen willst (z.B. absteigend, nach bestimmtem Feld, spezielle Regeln), kommt dein eigener Comparator ins Spiel.
Fazit: Comparator ist weniger Magie, als es aussieht
Wenn du den Kern verstanden hast – eine Methode, die -1, 0 oder 1 (bzw. allgemein <0, 0, >0) zurueckgibt – verliert Comparator schnell seinen Schrecken. Der Rest sind Komfortfunktionen und Syntax, die dir das Leben leichter machen.
Die wichtigsten Punkte zum Mitnehmen:
- Comparator ermöglicht dir flexible, externe Vergleichslogik.
- Nutze Comparator.comparing, thenComparing und reversed, um lesbare Sortierketten zu bauen.
- Achte auf saubere Vergleiche (kein Direkt-Subtrahieren), konsistente Regeln und Null-Werte.
- Wiederverwende Comparatoren, statt sie überall neu zu schreiben.
Wenn du das in ein paar kleinen Projekten ausprobierst - zum Beispiel beim Sortieren von DTO-Listen in einem REST-Service oder beim Aufbereiten von Tabellen für ein Frontend, wirst du merken: Comparator ist eines dieser Java-Werkzeuge, das man extrem häufig braucht und das sich schnell wie selbstverständlich anfühlt.
