W3docs

Java Comparable und Comparator

Natürliche Sortierung mit Comparable und externe Sortierung mit Comparator in Java definieren und kombinieren.

Zwei Interfaces, eine Aufgabe: Java mitteilen, wann ein Objekt „kleiner" als ein anderes ist. An der Aufrufstelle sehen sie fast identisch aus, und ihre Methoden geben sogar denselben Wertetyp zurück — einen negativen int, null oder einen positiven int. Der Unterschied liegt darin, wo die Ordnung festgelegt wird:

  • Comparable<T> — der Typ selbst weiß, wie seine Instanzen sortiert werden. Die Methode int compareTo(T other) ist die natürliche Ordnung des Typs.
  • Comparator<T> — ein externes Objekt, das Instanzen sortiert. Die Methode int compare(T a, T b) beschreibt eine von vielen möglichen Ordnungen.

Comparable implementiert man, wenn es ein offensichtliches „kleiner als" für einen Typ gibt — Integer, String, LocalDate. Einen Comparator schreibt man für jede andere Sortierung — nach Länge, nach Groß-/Kleinschreibung, nach absteigendem Preis, nach allem, was sich im Code ausdrücken lässt. Die meisten Typen haben ein Comparable (oder keines) und Dutzende nützliche Comparators.

Der Vertrag: −/0/+

Beide Methoden geben einen int zurück, dessen Vorzeichen die Antwort ist:

  • negativa kommt vor b
  • null — für Sortierzwecke gleich
  • positiva kommt nach b

Die genaue Größenordnung spielt keine Rolle. -1 und -1_000_000 bedeuten dasselbe. Niemals return a.size - b.size verwenden, wenn ein Überlauf möglich ist: Das Subtrahieren von Integer.MIN_VALUE von einer positiven Zahl führt zu einem Überlauf. Stattdessen Integer.compare(a.size(), b.size()) verwenden — das ist überlaufsicher und hat dieselbe Anzahl von Zeichen.

Comparable<T> — natürliche Ordnung

Ein Typ implementiert Comparable<Self> und stellt compareTo bereit:

public record Version(int major, int minor, int patch) implements Comparable<Version> {
  @Override public int compareTo(Version other) {
    int m = Integer.compare(this.major, other.major);
    if (m != 0) return m;
    int n = Integer.compare(this.minor, other.minor);
    if (n != 0) return n;
    return Integer.compare(this.patch, other.patch);
  }
}

Nun funktionieren Collections.sort(versions), versions.stream().sorted(), new TreeSet<Version>() und new TreeMap<Version, X>() alle ohne zusätzliche Argumente.

Der Vertrag hat drei Regeln, die jedes compareTo einhalten muss:

  1. Antisymmetriea.compareTo(b) und b.compareTo(a) haben entgegengesetzte Vorzeichen.
  2. Transitivität — wenn a < b und b < c, dann a < c.
  3. Konsistenz mit equals (dringend empfohlen)a.compareTo(b) == 0 genau dann, wenn a.equals(b).

Die dritte Regel wird am häufigsten versehentlich gebrochen. BigDecimal ist das bekannte Beispiel: new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) ist 0, aber .equals gibt false zurück. Dadurch werden ein TreeSet<BigDecimal> und ein HashSet<BigDecimal> unterschiedlich darüber urteilen, ob "1.0" und "1.00" Duplikate sind. Wenn möglich, sollte man sie konsistent halten.

Comparator<T> — externe Ordnung

Ein Comparator ist ein separates Objekt. Er kann beliebige zwei Ts vergleichen, auch Typen, die man selbst nicht geschrieben hat:

Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
list.sort(byLength);

Da Comparator<T> ein funktionales Interface ist (eine abstrakte Methode, compare), ist jeder Comparator nur ein Lambda oder eine Methodenreferenz. Das ist die moderne Form von Comparator-Code — man schreibt fast nie mehr eine vollständige anonyme Klasse.

Die Builder-Methoden auf Comparator

Die Klasse verfügt über statische Factory-Methoden, die das Erstellen von Comparatoren kurz und lesbar machen:

Comparator<Person> byAge       = Comparator.comparingInt(Person::age);
Comparator<Person> byName      = Comparator.comparing(Person::name);
Comparator<Person> byNameCi    = Comparator.comparing(Person::name, String.CASE_INSENSITIVE_ORDER);
Comparator<Person> oldestFirst = byAge.reversed();
Comparator<String> nullsFirst  = Comparator.nullsFirst(Comparator.naturalOrder());

Primitive Builder-Methoden sollte man verwenden — comparingInt, comparingLong, comparingDouble — wenn der Schlüssel ein primitiver Typ ist. Sie vermeiden Boxing bei jedem Vergleich, was sich bei einer langen Sortierung summiert.

Verkettete Comparatoren mit thenComparing

Ein weiterer Grund, die Builder-Methoden zu bevorzugen: Man kann mehrere Schlüssel verketten.

Comparator<Person> ordering =
    Comparator.comparing(Person::lastName)
              .thenComparing(Person::firstName)
              .thenComparingInt(Person::age);

Das liest sich von oben nach unten als „Primärschlüssel Nachname; Gleichstand durch Vorname auflösen; dann nach Alter." thenComparing wird auf dem vorherigen Comparator aufgerufen und gibt einen neuen zurück, der den zweiten Schlüssel nur dann heranzieht, wenn der erste einen Gleichstand ergab. Die Kette ist beliebig lang.

reversed(), nullsFirst, nullsLast

Drei Modifikatoren kommen häufig vor:

  • reversed() kehrt die Reihenfolge eines beliebigen Comparators um. byAge.reversed() bedeutet „Älteste zuerst."
  • nullsFirst(cmp) kapselt einen Comparator so, dass null-Werte als kleiner als jeder Nicht-null-Wert behandelt werden. Nützlich beim Sortieren von Collections, die null enthalten können.
  • nullsLast(cmp) ist das symmetrische Gegenstück.

reversed() darf nicht auf einem verketteten Comparator verwendet werden und erwartet, dass nur der letzte Schlüssel umgekehrt wird — reversed() kehrt die gesamte Ordnung um, jeden Schlüssel in der Kette.

Comparable vs. Comparator in JDK-APIs

Viele Methoden existieren in zwei Varianten — eine, die die natürliche Ordnung verwendet, und eine, die einen Comparator entgegennimmt:

OperationÜberladung mit natürlicher OrdnungÜberladung mit Comparator
Liste sortierenCollections.sort(list)Collections.sort(list, cmp)
Liste sortieren (modern)list.sort(null)list.sort(cmp)
Stream sortierenstream.sorted()stream.sorted(cmp)
Baumbasiertes Setnew TreeSet<>()new TreeSet<>(cmp)
Baumbasierte Mapnew TreeMap<>()new TreeMap<>(cmp)
Min/MaxCollections.min(list)Collections.min(list, cmp)
Binäre SucheCollections.binarySearch(list, key)Collections.binarySearch(list, key, cmp)
PriorityQueuenatürliche Ordnung des ElementtypsKonstruktor nimmt einen Comparator entgegen

Die Varianten mit natürlicher Ordnung erfordern, dass der Elementtyp Comparable implementiert. Wenn das nicht der Fall ist und man sie trotzdem aufruft, erhält man zur Laufzeit eine ClassCastException — keinen Compilerfehler — da der Cast innerhalb der Sortierungsimplementierung stattfindet.

Ein praktisches Beispiel: natürliche Ordnung, benutzerdefinierte Comparatoren, verkettete Schlüssel, Nullwerte

Das folgende Programm definiert einen Record mit einer natürlichen Ordnung (Comparable) sowie drei externe Ordnungen: nach einem einzelnen Schlüssel, nach verketteten Schlüsseln mit umgekehrtem Sekundärschlüssel und eine, die null-Einträge toleriert.

java— editable, runs on the server

Was sich aus der Ausgabe ergibt:

  • Die Comparable-Implementierung hat nach Name sortiert und Namensgleichstände nach Alter aufgelöst. Kein expliziter Comparator war nötig — natürliche Ordnung ist der Standard für Collections.sort und verwandte Methoden.
  • Comparator.comparingDouble(Person::salary) ist kürzer und schneller als (a, b) -> Double.compare(a.salary(), b.salary()), da es Boxing vermeidet.
  • Der verkettete Comparator sortierte primär nach Alter und verwendete reversed() nur für den Gehalt-Schlüssel — das ist das richtige Muster, wenn man für verschiedene Schlüssel unterschiedliche Richtungen möchte. Im Vergleich dazu würde .reversed() auf der gesamten Kette beide Schlüssel umkehren.
  • nullsFirst ermöglichte es dem Comparator, eine Liste mit null-Einträgen zu verarbeiten, ohne eine NullPointerException auszulösen. Ohne diesen Wrapper würde der erste Vergleich mit einem null abstürzen.
  • Der „Subtraktionstrick" lieferte die falsche Antwort für Integer.MAX_VALUE - (-1): Diese Berechnung läuft über und ergibt eine negative Zahl, sodass bad MAX_VALUE als kleiner als -1 meldet. Integer.compare liefert jederzeit das richtige Vorzeichen. Immer bevorzugen.

Was kommt als Nächstes

Jetzt sind Iteration (Iterator / ListIterator) und Ordnung (Comparable / Comparator) abgedeckt. Das nächste Kapitel fasst beides in der Hilfsklasse java.util.Collections zusammen — dem statischen Werkzeugkasten mit sort, search, reverse, shuffle, min, max und „diese Collection als unveränderlich kapseln"-Methoden, die auf beliebigen List-, Set- oder Map-Instanzen arbeiten. Danach behandeln zwei kurze Kapitel speziell das Sortieren und Suchen.

Übungen

Übung
Du schreibst `list.sort((a, b) -> a.scoreDifference(b))`, wobei `scoreDifference` `a.score - b.score` als `int` zurückgibt. Die Liste enthält Werte einschließlich `Integer.MAX_VALUE` und `Integer.MIN_VALUE`, und das Ergebnis ist offensichtlich falsch. Was ist die Lösung?
Du schreibst `list.sort((a, b) -> a.scoreDifference(b))`, wobei `scoreDifference` `a.score - b.score` als `int` zurückgibt. Die Liste enthält Werte einschließlich `Integer.MAX_VALUE` und `Integer.MIN_VALUE`, und das Ergebnis ist offensichtlich falsch. Was ist die Lösung?
Was this page helpful?