Zum Inhalt springen

Comparator, Comparable, compareTo – Java-Objekte vergleichen

Drei Wippen aus hellem Holz auf Messingkegeln vor Koralle, auf jeder zwei leuchtende weiße Kugeln: Die linke neigt sich nach rechts zur größeren Kugel, die mittlere trägt zwei gleich große und liegt waagerecht, die rechte neigt sich nach links zur größeren Kugel

Dieser Artikel erklärt:

  • Wie vergleicht man zwei Objekte in Java?
  • Was ist ein Comparator und wofür benötigt man ihn?
  • Welche Möglichkeiten gibt es, um einen Comparator zu erzeugen?
  • Wie erzeugt man einen Comparator mit Comparator.comparing()?
  • Was ist der Unterschied zwischen Comparator und Comparable?

Doch vorab die Frage: Warum möchte man überhaupt Java-Objekte miteinander vergleichen?

Das wichtigste Einsatzgebiet für den Vergleich zweier Objekte ist das Sortieren von Objekt-Listen oder -Arrays. Um Objekte zu sortieren, muss das Programm sie vergleichen und herausfinden, ob ein Objekt kleiner, größer oder gleich einem anderen ist.

Die Interfaces Comparable und Comparator, um die es in diesem Artikel geht, steuern auch die Reihenfolge in TreeMap und TreeSet, den Kopf einer PriorityQueue, das Ergebnis von Collections.max() und die Sortierung in Stream.sorted().

Den Quellcode zum Artikel findest du im zugehörigen GitHub-Repository.

Wie vergleicht man zwei Objekte in Java?

Java-Primitive (int, long, double, etc.) vergleicht man mit den Operatoren <, <=, ==, !=, >=, >.

Bei Objekten funktioniert das nicht: < und > lässt der Compiler nicht zu, und == prüft nur, ob es dasselbe Objekt ist.

Dort verwendet man stattdessen entweder eine compare()- oder eine compareTo()-Methode. Beide Methoden erkläre ich am Beispiel von Strings und der selbst geschriebenen Klasse Student.

Wie vergleicht man zwei Strings in Java?

Nehmen wir an, wir haben die folgenden zwei Strings:

String s1 = "Happy";
String s2 = "Coders";

Wir wollen nun herausfinden, ob s1 kleiner, größer oder gleich s2 ist. Oder anders gesagt: ob – nach alphabetischer Sortierung – s1 vor oder nach s2 käme.

Das tun wir wie folgt:

int result = s1.compareTo(s2);

Die Variable result enthält nun:

  • einen Wert kleiner als 0, wenn s1 nach alphabetischer Sortierung vor s2 kommt
  • 0, wenn s1 und s2 gleich sind (d. h., dass s1.equals(s2) true ergibt)
  • einen Wert größer als 0, wenn s1 nach alphabetischer Sortierung nach s2 kommt

Im Beispiel oben wäre result größer als 0, da „Happy“ hinter „Coders“ einsortiert werden würde.

Sortieren von Strings in Java

Die compareTo()-Methode wird intern von Arrays.sort() und Collections.sort() aufgerufen, um ein Array oder eine Liste von Objekten zu sortieren, beispielsweise wie folgt:

public class NameSortExample {
  public static void main(String[] args) {
    String[] names = {"Mary", "James", "Patricia", "John", "Jennifer", "Robert"};
    Arrays.sort(names);
    System.out.println(Arrays.toString(names));
  }
}

Das Programm gibt die Namen in alphabetischer Reihenfolge aus:

[James, Jennifer, John, Mary, Patricia, Robert]

Alle weiteren Wege, Objekte und Primitive wie int, long und double zu sortieren, zeigt das Tutorial „Sortieren in Java“.

Was aber, wenn wir Strings gar nicht alphabetisch sortieren wollen, sondern beispielsweise nach ihrer Länge? Dazu brauchen wir einen sogenannten Comparator. Bevor wir dazu kommen, erkläre ich erst einmal das Interface Comparable, das wir soeben verwendet haben.

Das Java-Comparable-Interface

Die String.compareTo()-Methode, die wir oben angewendet haben, kommt aus dem java.lang.Comparable-Interface, welches von der String-Klasse implementiert wird.

Das Comparable-Interface definiert nur diese eine Methode. Es wird von allen Klassen implementiert, deren Objekte vergleichbar (englisch: „comparable“) sein sollen. Neben String sind das beispielsweise Integer, Long, BigInteger, LocalDate, LocalDateTime, Instant und viele mehr.

Die Reihenfolge, die sich aus der compareTo()-Methode ergibt, nennt man „natürliche Ordnung“: Strings werden alphabetisch sortiert, Datums- und Zeitwerte in ihrer zeitlichen Abfolge.

Für compareTo() gilt ein sogenannter Contract (eine Vereinbarung, an die sich jede Implementierung halten muss): Das Vorzeichen dreht sich um, wenn du die Seiten tauschst (sgn(a.compareTo(b)) == -sgn(b.compareTo(a))), und die Ordnung ist transitiv – ist a kleiner als b und b kleiner als c, dann ist a kleiner als c. Verletzt eine Implementierung den Contract, ist das Sortierergebnis undefiniert. Mitunter fällt es dem JDK selbst auf: Der Sortieralgorithmus TimSort wirft seit Java 7 eine IllegalArgumentException mit der Meldung „Comparison method violates its general contract!“.

Wie du eine selbst entwickelte Klasse vergleichbar (und damit sortierbar) machst, liest du im folgenden Abschnitt.

Java-Comparable-Beispiel

Um selbstentwickelte Klassen vergleichbar und damit sortierbar zu machen, müssen sie das Interface Comparable und dessen compareTo()-Methode implementieren.

Das folgende Beispiel zeigt, wie man dies für eine Student-Klasse macht, deren Objekte standardmäßig nach Matrikelnummer (ID) sortiert werden sollen:

public class Student implements Comparable<Student> {
  private int id;
  private String firstName;
  private String lastName;

  // ... constructor ...
  // ... getters and setters ...
  // ... toString() method ...

  @Override
  public int compareTo(Student o) {
    if (this.id < o.id) {
      return -1;
    } else if (this.id == o.id) {
      return 0;
    } else {
      return 1;
    }
  }
}

Mit dem ternären Operator kannst du die compareTo()-Methode auch als Einzeiler notieren:

@Override
public int compareTo(Student o) {
  return this.id < o.id ? -1 : (this.id == o.id ? 0 : 1);
}

Noch eleganter ist es allerdings, für den Vergleich der zwei int-Werte die statische Methode Integer.compare() aufzurufen:

@Override
public int compareTo(Student o) {
  return Integer.compare(this.id, o.id);
}

Der JIT-Compiler inlined den Aufruf von Integer.compare(); er kostet zur Laufzeit nichts.

Und hier ein Beispiel, in dem drei Student-Objekte erzeugt, in eine Liste geschrieben und anschließend sortiert werden:

public class StudentSortExample {
  public static void main(String[] args) {
    List<Student> students = new ArrayList<>();
    students.add(new Student(47271, "Kerrie", "Adkins"));
    students.add(new Student(99319, "Aarron", "Wicks"));
    students.add(new Student(11056, "Kaya", "Molina"));

    Collections.sort(students);

    System.out.println("students = " + students);
  }
}

Wie erwartet, werden die Student-Objekte nach Matrikelnummer sortiert ausgegeben (die Zeilenumbrüche habe ich der Übersicht halber manuell eingefügt):

students = [Student{id=11056, firstName='Kaya', lastName='Molina'},
    Student{id=47271, firstName='Kerrie', lastName='Adkins'},
    Student{id=99319, firstName='Aarron', lastName='Wicks'}]

Kommen wir zurück zum String, den wir im nächsten Schritt nicht alphabetisch, sondern nach Länge sortieren wollen.

Das Java-Comparator-Interface

Um zwei Objekte nach einer anderen als ihrer natürlichen Ordnung zu sortieren (oder um Objekte von Klassen zu sortieren, die Comparable nicht implementieren), verwenden wir das java.util.Comparator-Interface.

Dieses definiert die Methode compare(T o1, T o2), mit der die zwei übergebenen Objekte verglichen werden. Die Methode hat analog zur compareTo()-Methode folgende Rückgabewerte:

  • einen Wert kleiner als 0, wenn o1 kleiner ist als o2
  • 0, wenn o1 und o2 in dieser Ordnung gleichrangig sind – das heißt nicht, dass o1.equals(o2) true ergibt: Der Längen-Comparator im nächsten Abschnitt liefert für „Mary“ und „John“ 0
  • einen Wert größer als 0, wenn o1 größer ist als o2

Java-Comparator-Beispiel: Strings nach Länge sortieren

Einen Comparator, der Strings nach ihrer Länge vergleicht, würden wir wie folgt implementieren:

public class StringLengthComparator implements Comparator<String> {
  @Override
  public int compare(String o1, String o2) {
    if (o1.length() < o2.length()) {
      return -1;
    } else if (o1.length() == o2.length()) {
      return 0;
    } else {
      return 1;
    }
  }
}

Auch hier können wir den Code mit Hilfe des ternären Operators wieder auf eine Zeile komprimieren:

@Override
public int compare(String o1, String o2) {
  return o1.length() < o2.length() ? -1 : (o1.length() == o2.length() ? 0 : 1);
}

Eingesetzt wird der StringLengthComparator beispielsweise wie folgt:

public class NameSortByLengthExample {
  public static void main(String[] args) {
    String[] names = {"Mary", "James", "Patricia", "John", "Jennifer", "Robert"};
    Arrays.sort(names, new StringLengthComparator());
    System.out.println(Arrays.toString(names));
  }
}

Die Namen werden nun nicht mehr alphabetisch sortiert, sondern aufsteigend nach Länge:

[Mary, John, James, Robert, Patricia, Jennifer]

Wie erstellt man einen Comparator?

Bis Java 7 konnte man einen Comparator ausschließlich – wie im Beispiel oben gezeigt – durch Implementierung des Comparator-Interfaces erstellen.

Seit Java 8 kann man einen Comparator auch als Lambda notieren oder ihn – wie du gleich sehen wirst – mit Hilfe der Methoden Comparator.comparing(), thenComparing() und reversed() erzeugen.

Comparator als öffentliche Klasse

Am Beispiel StringLengthComparator haben wir bereits die erste Variante kennengelernt: Wir schreiben eine öffentliche Klasse und übergeben der Sortiermethode eine Instanz davon:

Arrays.sort(names, new StringLengthComparator());

Wenn wir an mehreren Stellen nach Stringlänge sortieren wollen, können wir auch eine Konstante extrahieren:

private static final StringLengthComparator STRING_LENGTH_COMPARATOR =
    new StringLengthComparator();

Alternativ könnten wir ein Singleton definieren:

public class StringLengthComparator implements Comparator<String> {
  public static final StringLengthComparator INSTANCE = new StringLengthComparator();

  private StringLengthComparator() {}

  @Override
  public int compare(String o1, String o2) {
    return Integer.compare(o1.length(), o2.length());
  }
}

Eine öffentliche Klasse gibt uns auch die Möglichkeit, das Sortierverhalten über Konstruktor-Parameter zu steuern. Z. B. könnten wir die Sortierreihenfolge konfigurierbar machen:

public class StringLengthComparator implements Comparator<String> {
  public static final StringLengthComparator ASC = new StringLengthComparator(true);
  public static final StringLengthComparator DESC = new StringLengthComparator(false);

  private final boolean ascending;

  private StringLengthComparator(boolean ascending) {
    this.ascending = ascending;
  }

  @Override
  public int compare(String o1, String o2) {
    int result = Integer.compare(o1.length(), o2.length());
    return ascending ? result : -result;
  }
}

Diesen Comparator würden wir beispielsweise wie folgt einsetzen, um unsere Namensliste absteigend zu sortieren:

Arrays.sort(names, StringLengthComparator.DESC);

Eine öffentliche Klasse lässt uns also die meisten Freiheiten bei der Definition unseres Comparators – für die Sortierrichtung brauchen wir sie seit Java 8 nicht mehr, wie der Abschnitt „Absteigend sortieren mit reversed()“ zeigt.

Comparator als anonyme Klasse

Benötigen wir einen Comparator nur an einer einzigen Stelle, können wir ihn auch als anonyme Klasse definieren.

Mit dem folgenden Code beispielsweise sortieren wir unsere Student:innen nach Nachnamen:

students.sort(new Comparator<Student>() {
  @Override
  public int compare(Student o1, Student o2) {
    return o1.getLastName().compareTo(o2.getLastName());
  }
});

Die Methode List.sort() gibt es ebenfalls erst seit Java 8; davor schreibt man Collections.sort(students, comparator).

Da es Nachnamen gibt, die häufiger vorkommen, sollten wir nach Nachnamen und nach Vornamen sortieren. Dies tun wir, indem wir zunächst prüfen, ob die Nachnamen gleich sind. Wenn das der Fall ist, vergleichen wir auch die Vornamen:

students.sort(new Comparator<Student>() {
  @Override
  public int compare(Student o1, Student o2) {
    int result = o1.getLastName().compareTo(o2.getLastName());
    if (result == 0) {
      result = o1.getFirstName().compareTo(o2.getFirstName());
    }
    return result;
  }
});

In beiden Fällen wird uns eine moderne IDE wie IntelliJ darauf hinweisen, dass das ab Java 8 auch eleganter geht (und uns im besten Fall auch gleich anbieten, den Code zu refactoren):

Comparator-Refactoring zum Lambda-Ausdruck

Was dabei herauskommt, erfährst du im nächsten Abschnitt.

Java-Comparator als Lambda notieren

Ab Java 8 können wir statt der anonymen Klasse nämlich auch ein Lambda verwenden. Das Sortieren nach Nachname erfolgt dann wie folgt:

students.sort((o1, o2) -> o1.getLastName().compareTo(o2.getLastName()));

Auch das Sortieren nach Nach- und Vorname wird durch die Lambda-Notation kürzer:

students.sort((o1, o2) -> {
  int result = o1.getLastName().compareTo(o2.getLastName());
  if (result == 0) {
    result = o1.getFirstName().compareTo(o2.getFirstName());
  }
  return result;
});

Kürzer geht es mit der Methode, die ich im folgenden Abschnitt zeige. Auch den Schritt dorthin bietet uns eine moderne IDE an:

Refactoring zur Comparator Chain

Comparator erzeugen mit Comparator.comparing()

Am kürzesten wird ein Comparator mit Comparator.comparing() und den darauf aufrufbaren Methoden thenComparing() und reversed() – ebenfalls seit Java 8, samt Varianten für die primitiven Datentypen int, long und double.

Um die Student:innen nach Nachnamen zu sortieren, schreiben wir folgendes:

students.sort(Comparator.comparing(Student::getLastName));

Wir übergeben also lediglich eine Referenz auf die Methode, die den Wert liefert, nach dem sortiert werden soll. Der Javadoc nennt diesen Wert den sogenannten Schlüssel (englisch „sort key“) und den Parameter von comparing() entsprechend keyExtractor – der Begriff kommt weiter unten bei den null-Werten wieder.

Nach mehreren Kriterien sortieren mit thenComparing()

Nach Nach- und Vornamen sortieren wir wie folgt:

students.sort(Comparator.comparing(Student::getLastName)
    .thenComparing(Student::getFirstName));

Diese Schreibweise macht es uns einfach, auch den Fall abzudecken, dass zwei Student:innen nicht nur den gleichen Nach-, sondern auch den gleichen Vornamen haben. Um sie in diesem Fall zusätzlich nach ID zu sortieren, müssen wir lediglich ein thenComparingInt() hinzufügen:

students.sort(Comparator.comparing(Student::getLastName)
    .thenComparing(Student::getFirstName)
    .thenComparingInt(Student::getId));

Comparator.comparing() und die damit aufgebauten Comparator-Ketten machen den Code nicht nur kürzer, sondern auch aussagekräftiger: Du siehst auf den ersten Blick, wonach sortiert wird.

Absteigend sortieren mit reversed()

Absteigend sortierst du mit reversed():

students.sort(Comparator.comparing(Student::getLastName).reversed());

reversed() dreht die gesamte Kette um, die vor ihm steht – bei comparing(…).thenComparing(…).reversed() also beide Kriterien. Der StringLengthComparator mit den Konstanten ASC und DESC aus dem Abschnitt „Comparator als öffentliche Klasse“ schrumpft damit auf eine Zeile:

Arrays.sort(names, Comparator.comparingInt(String::length).reversed());

Für die natürliche Ordnung und ihre Umkehrung gibt es Comparator.naturalOrder() und Comparator.reverseOrder().

null-Werte sortieren mit nullsFirst() und nullsLast()

Comparator.comparing() wirft eine NullPointerException, sobald der Schlüssel eines Elements null ist – im Beispiel also ein fehlender Nachname. Dagegen hilft der zweite Parameter von comparing(), ein Comparator für den Schlüssel – hier Comparator.nullsLast(), der null ans Ende sortiert und alle anderen Schlüssel an den übergebenen Comparator weiterreicht:

students.sort(Comparator.comparing(Student::getLastName,
    Comparator.nullsLast(Comparator.naturalOrder())));

Objekte ohne Nachnamen landen damit am Ende, mit nullsFirst() am Anfang. Ein Comparator.nullsLast(comparator) um die ganze Kette behandelt dagegen null als Element der students-Liste, nicht als Schlüssel.

Streams sortieren mit Comparator.comparing()

Dieselbe Comparator-Kette sortiert auch einen Stream:

List<Student> sorted = students.stream()
    .sorted(Comparator.comparing(Student::getLastName))
    .toList();

Comparable vs. Comparator – Zusammenfassung

Wir haben im Laufe des Artikels die Interfaces java.lang.Comparable und java.util.Comparator kennengelernt. Die Tabelle stellt sie gegenüber:

ComparableComparator
Packagejava.langjava.util
MethodecompareTo(T o)compare(T o1, T o2)
Implementiert vonder Klasse selbsteinem eigenen Objekt
Ordnungen je Klassegenau eine, die natürliche Ordnungbeliebig viele
Erzeugenimplements Comparable<T>Klasse, anonyme Klasse, Lambda oder Comparator.comparing()
Typischer AufrufCollections.sort(list)list.sort(comparator)

Mit einem Comparator können wir außerdem Objekte sortieren, die das Comparable-Interface nicht implementieren, also keine natürliche Ordnung haben.

Seit Java 8 lassen sich Comparatoren in einer Zeile definieren, wie z. B. in students.sort(Comparator.comparing(Student::getLastName)).

Meine Empfehlung: Comparable nur für die eine offensichtliche Ordnung einer Klasse – wie ID oder Zeitpunkt –, jede andere Ordnung als Comparator mit Comparator.comparing(). Und compareTo() nie per Subtraktion.

Wenn dir dieser Artikel gefallen hat, freue ich mich über eine Bewertung auf meinem ProvenExpert-Profil.

👉 Bewertung abgeben

Lust auf noch mehr Wissen?

In meinem Blog findest du viele Artikel zu Java, Softwarearchitektur und Performance von grundlegenden Konzepten bis hin zu fortgeschrittenen Patterns.

Wenn du tiefer einsteigen willst, schau dir meine Trainings an: praxisnah, verständlich und direkt auf euren Projektalltag übertragbar. Statt Theorie vermittle ich Prinzipien, die euch helfen, Code langfristig besser, wartbarer und performanter zu schreiben.

Zu den Java-Schulungen

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche