Comparator, Comparable und compareTo – Vergleichen von Objekten in Java - Feature-Bild

Comparator, Comparable und compareTo – Vergleichen von Objekten in Java

von Sven Woltmann – 6. Oktober 2020

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 Java 8?
  • 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 sicherlich 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.

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

Wie vergleicht man zwei Objekte in Java?

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

Bei Objekten funktioniert das nicht.

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.

(* Alphabetisch bedeutet im Fall von String.compareTo(): entsprechend der Unicode-Werte der Buchstaben, d. h. beispielsweise, dass ein Großbuchstabe immer vor einem Kleinbuchstaben kommt, und dass die deutschen Umlaute erst nach allen regulären Groß- und Kleinbuchstaben kommen)

Sortieren von Strings in Java

Die compareTo()-Methode wird hinter den Kulissen verwendet, 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]

(Welche Möglichkeiten es noch gibt, um in Java Objekte oder Primitive, wie int, long, double zu sortieren, erfährst du im 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, BigInteger, Long, Date, LocalDateTime, 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.

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, die standardmäßig nach Matrikelnummer (ID) sortiert werden sollen:

public class Student implements Student<Customer> {
  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(Customer 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(Customer o) {
  return Integer.compare(this.id, o.id);
}

Keine Sorge, die JVM optimiert diesen Methodenaufruf weg, so dass es zu keinerlei Performance-Einbußen kommt.

Achtung Falle: ints subtrahieren

Mitunter sieht man in einer compareTo()-Methode folgendes:

return this.id - o.id;

Das sollte man auf keinen Fall so schreiben, denn es funktioniert nicht, wenn beispielsweise this.id -2.000.000.000 ist und o.id 1.000.000.000. Da in diesem Fall this.id kleiner ist, sollte die compareTo()-Methode eine negative Zahl zurückliefern. Das tut sie aber nicht, da es bei der Subtraktion zu einem arithmetischen Unterlauf kommt – das Ergebnis der Subtraktion ist 1.294.967.296 – eine positive Zahl!

Und hier ein Beispiel, in dem drei Studenten 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 Studenten 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ürlicher 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 gleich sind (d. h., dass o1.equals(o2) true ergibt);
  • 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 Interfaces Comparator erstellen.

Seit Java 8 kann man einen Comparator auch als Lambda notieren oder ihn – ziemlich komfortabel, 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 gibt uns also größtmögliche Freiheit und Flexibilität bei der Definition unseres Comparators.

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 Studenten nach Nachnamen:

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

Da es Nachnamen gibt, die häufiger vorkommen, sollten wir vielleicht besser nach Nachnamen und nach Vornamen sortieren. Dies tun wir, in dem 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 optimalerweise 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;
});

Richtig schön wird es mit der Methode, die ich im folgenden Abschnitt zeigen werde. Auch den Schritt dorthin bietet uns eine moderne IDE an:

Refactoring zur Comparator Chain

Java 8: Comparator erzeugen mit Comparator.comparing()

Die eleganteste Methode zur Konstruktion eines Comparators, die ebenfalls seit Java 8 zur Verfügung steht, ist die Verwendung von Comparator.comparing(), Comparator.thenComparing() und Comparator.reversed() (sowie deren Variationen für die primitiven Datentypen int, long und double).

Um die Studenten nach Nachnamen zu sortieren, schreiben wir folgendes:

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

Wir übergeben also lediglich eine Referenz auf die Methode, die das Feld zurückgibt, nach dem sortiert werden soll.

Nach Nach- und Vornamen sortieren wir wie folgt:

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

Diese Schreibweise macht es uns sehr einfach auch den Fall abzudecken, dass zwei Studenten 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 deutlich aussagekräftiger.

Comparable vs Comparator – Zusammenfassung

Wir haben im Laufe des Artikels die Interfaces java.lang.Comparable und java.util.Comparator kennengelernt. Hier noch einmal der Unterschied in wenigen Sätzen zusammengefasst:

Durch die Implementierung der Comparable.compareTo()-Methode definieren wir die natürliche Ordnung von Objekten einer Klasse, d. h. diejenige Ordnung, nach der die Objekte der Klasse standardmäßig sortiert werden, also beispielsweise durch Arrays.sort(arrayOfObjects) oder Collections.sort(listOfObjects).

Mit einem Comparator können wir Objekte in einer anderen als ihrer natürlichen Ordnung sortieren. Und wir können Objekte sortieren, die das Comparable-Interface nicht implementieren, also keine natürliche Ordnung haben. Seit Java 8 lassen sich Comparatoren sehr elegant definieren, wie z. B. in students.sort(Comparator.comparing(Student::getLastName).

Wenn dir der Artikel gefallen hat und du informiert werden möchtest, wenn ich weitere Artikel veröffentliche, dann trage dich gerne über das folgende Formular in meinen E-Mail-Verteiler ein. Oder teile den Artikel über einen der Share-Buttons im Anschluss.

  •  
  •  
  •  
  •  
  •  
  •  

Über den Autor

Ich bin freiberuflicher Softwareentwickler mit über 20 Jahren Erfahrung in skalierbaren Java-Enterprise-Anwendungen. Mein Schwerpunkt liegt auf der Optimierung komplexer Algorithmen und auf fortgeschrittenen Themen wie Concurrency, dem Java Memory Model und Garbage Collection. Hier auf HappyCoders.eu möchte ich dir helfen, ein besserer Java-Programmierer zu werden. Lies mehr über mich hier.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Pflichtfelder sind markiert.

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}