1. Einführung
Im Leben reicht selten nur eine einzige Art, Objekte zu vergleichen. Stellen Sie sich vor, Sie haben eine Liste von Benutzern: Manchmal möchten Sie sie nach dem Namen sortieren, manchmal – nach dem Alter, und manchmal – nach der Länge des Nachnamens. Oder es gibt eine Klasse, deren Autor nicht Sie sind, und Sie können ihr compareTo nicht hinzufügen. Genau für solche Fälle gibt es in Java das Interface Comparator.
Wenn Comparable nicht ausreicht
- Die Klasse darf nicht geändert werden (z. B. stammt sie aus einer Fremdbibliothek).
- Es werden mehrere Sortierweisen benötigt (nach verschiedenen Feldern).
- Sie möchten die Vergleichslogik von der Klasse trennen (z. B. in verschiedenen Teilen des Programms unterschiedlich sortieren).
Analogie
Wenn Comparable die eingebaute „natürliche Ordnung“ eines Objekts ist, dann ist Comparator ein externer Schiedsrichter, der Ihre Objekte nach beliebigen Kriterien bewerten kann: heute nach dem Namen, morgen – nach dem Alter, übermorgen – nach der Namenslänge.
2. Comparator-Interface: Syntax und Vertrag
Deklaration des Interfaces
public interface Comparator<T> {
int compare(T o1, T o2);
}
Die Methode compare muss zurückgeben:
- Eine negative Zahl, wenn das erste Objekt „kleiner“ als das zweite ist.
- 0, wenn sie gleich sind.
- Eine positive Zahl, wenn das erste „größer“ als das zweite ist.
Der Vertrag ist derselbe wie bei Comparable, nur dass jetzt zwei Objekte verglichen werden und nicht „dieses“ und „ein anderes“ über compareTo.
Beispiel: Comparator zum Sortieren nach Nachnamen
Nehmen wir an, wir haben die Klasse Person:
public class Person {
private String firstName;
private String lastName;
private int age;
// Konstruktor und Getter
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; }
}
Erstellen wir einen Comparator, der nach dem Nachnamen sortiert:
import java.util.Comparator;
public class LastNameComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return a.getLastName().compareTo(b.getLastName());
}
}
Hinweis: Die Methode compareTo bei Strings (String) vergleicht sie in alphabetischer Reihenfolge.
3. Verwendung von Comparator: Sammlungen sortieren
Sortieren mit einem Comparator
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Anna", "Kostetskaya", 25));
people.add(new Person("Boris", "Novak", 20));
people.add(new Person("Viktoriya", "Bell", 22));
// Nach dem Nachnamen sortieren
Collections.sort(people, new LastNameComparator());
for (Person p : people) {
System.out.println(p.getLastName() + " " + p.getFirstName());
}
}
}
Ergebnis:
Novak Boris
Kostetskaya Anna
Bell Viktoriya
Sortieren nach Alter mit einem Comparator
Selbst wenn die Klasse bereits Comparable nach dem Namen implementiert, kann ein separater Comparator – nach dem Alter – erstellt werden:
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return Integer.compare(a.getAge(), b.getAge());
}
}
Und analog verwendet werden:
Collections.sort(people, new AgeComparator());
Ergebnis:
Boris Novak (20)
Viktoriya Bell (22)
Anna Kostetskaya (25)
Beispiel: Comparator „on the fly“ auswählen
Collections.sort(people, new LastNameComparator()); // Nach dem Nachnamen
Collections.sort(people, new AgeComparator()); // Nach dem Alter
4. Anonyme Klassen und Lambda‑Ausdrücke
Comparators lassen sich „on the fly“ erstellen, ohne separate Klassen zu deklarieren.
Anonyme Klasse
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getFirstName().compareTo(b.getFirstName());
}
});
Lambda-Ausdruck
Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));
Oder noch kürzer mit der Listenmethode List.sort:
people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
- Anonyme Klassen – die alte Methode, umständlich.
- Lambda – modern und kompakt.
5. Beispiele: nach verschiedenen Kriterien sortieren
Sortieren nach Länge des Nachnamens
Comparator<Person> byLastNameLength = (a, b) ->
Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);
Sortieren nach Alter, dann nach Namen (mehrstufig)
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);
Comparator für die Suche verwenden (Beispiel)
Ein Comparator ist nicht nur fürs Sortieren geeignet, sondern auch für die Suche in sortierten Sammlungen:
// people müssen nach Alter sortiert sein!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
System.out.println("Person mit Alter 22 gefunden: " + people.get(idx));
}
6. Best Practices und Besonderheiten im Umgang mit Comparator
Den Vertrag nicht verletzen
- Wenn compare(a, b) 0 zurückgibt, muss compare(b, a) ebenfalls 0 zurückgeben.
- Wenn compare(a, b) > 0 ist, muss compare(b, a) < 0 sein.
- Berücksichtigen Sie mögliche null-Werte (siehe unten).
equals und hashCode nicht vergessen
Obwohl Comparators Objekte „auf ihre Weise“ vergleichen, ist es für Strukturen wie TreeSet oder bei der Schlüsselsuche in TreeMap wichtig, dass die Vergleichslogik des Comparators mit equals übereinstimmt. Andernfalls kann es zu unerwarteten Ergebnissen kommen: Zwei unterschiedliche Objekte gelten per Comparator als gleich, sind aber per equals nicht gleich.
Sortieren unter Berücksichtigung von null
Wenn Felder null sein können, verwenden Sie „fertige“ Helfer:
Comparator<Person> byLastNameNullSafe = Comparator.comparing(
Person::getLastName,
Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);
7. Nützliche Feinheiten
Tabelle: Vergleich von Comparable und Comparator
| Comparable | Comparator | |
|---|---|---|
| Wo wird es implementiert? | Im Klassen‑Typ selbst | In einer separaten Klasse/Lambda |
| Methode | |
|
| Wie viele Varianten? | Nur eine „natürliche“ | Beliebig viele, für alle Zwecke |
| Verwendung | |
|
| Auch für fremde Klassen? | Nein | Ja |
Beispiel: absteigend sortieren
Die Reihenfolge kann manuell invertiert werden:
Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);
Oder mit reversed():
Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());
8. Typische Fehler beim Umgang mit Comparator
Fehler Nr. 1: Verletzung des Vergleichsvertrags. Wenn Sie vergessen, dass compare(a, b) und compare(b, a) entgegengesetzte Vorzeichen haben müssen, oder beliebige Werte zurückgeben (z. B. einfach die Differenz – a.getAge() - b.getAge(), was zu Überläufen führen kann), wird das Ergebnis unvorhersehbar. Verwenden Sie Integer.compare statt Subtraktion – das ist sicherer.
Fehler Nr. 2: Ignorieren von null-Werten. Wenn Felder, nach denen Sie vergleichen, null sein können, behandeln Sie diesen Fall unbedingt (z. B. mit Comparator.nullsFirst/Comparator.nullsLast), sonst erhalten Sie leicht eine NullPointerException im unpassendsten Moment.
Fehler Nr. 3: Instabile Sortierkriterien. Wenn ein Comparator für dieselben Objekte unterschiedliche Werte liefert (z. B. eine Zufallszahl verwendet oder sich stark verändernde Felder), kann sich die Sortierung chaotisch verhalten.
Fehler Nr. 4: Inkonsistenz mit equals. Wenn compare(a, b) == 0, aber a.equals(b) false ist, funktionieren Sammlungen wie TreeSet und TreeMap möglicherweise nicht wie erwartet. Wünschenswert ist, dass Gleichheit per Comparator und per equals übereinstimmt.
Fehler Nr. 5: Sortieren fremder Klassen ohne Comparator. Wenn Sie Objekte einer „fremden“ Klasse ohne Comparable und ohne Übergabe eines Comparator sortieren wollen, erhalten Sie einen Kompilierfehler. Geben Sie einen expliziten Comparator an.
GO TO FULL VERSION