Java 8: Erweiterung von java.util.Comparator
Vor über einem Jahr habe ich darüber geschrieben, wie umständlich und aufwändig die Implementierung der Schnittstellen Comparable
und Comparator
in Java ist. Damals habe ich auch gezeigt, dass sich diese Aufgabe mit Lambda-Ausdrücken und Key-Funktionen wesentlich einfacher lösen lässt. Nun wird es Zeit für einen Blick in die Standardbibliothek von Java 8. Tatsächlich wird dieses Thema durch die neue Version adressiert. Eine entscheidende Rolle spielen dabei die beiden genannten Konzepte. Wie das aussieht, zeige ich an ein einigen Beispielen.
Für die Beispiele orientiere ich mich an einem Filmverleih. Das folgende Listing enthält die Definition der Schnittstelle Movie
, und Objekte dieses Typs werden unten nach verschiedenen Kriterien sortiert. Dabei versuche ich den Beispielen durch die unterschiedlichen Typen der Eigenschaften mehr Natürlichkeit zu verleihen.
enum Genre { ACTION, ADVENTURE, COMEDY, DOCUMENTARY, ...; }
interface Movie {
String getTitle();
Genre getGenre();
int getReleaseYear();
BigDecimal getRentalPrice();
}
Naheliegend ist die Sortierung nach Filmtitel. Dafür braucht es eigentlich einen Comparator
, dessen compareTo
-Methode erstens zwei Movie
-Objekte entgegennimmt, zweitens die beiden Filmtitel extrahiert und drittens den Vergleich an die extrahierten Teile delegiert. Viele Comparator
-Implementierungen funktionieren auf diese Weise. Da jedoch nur der zweite Schritt spezifisch ist, bietet sich eine generische Implementierung an, die durch eine sogenannte Key-Funktion parametrisiert wird.
Die statische Methode Comparator.comparing
übernimmt diese Aufgabe in Java 8. Eingebunden über einen statischen Import und parametrisiert mit einer Methoden-Referenz sieht die Sortierung nach Filmtitel somit wie folgt aus.
List<Movie> movies = ...;
movies.sort(comparing(Movie::getTitle));
Allerdings werden die Filmtitel dabei lexikographisch nach Codepoints sortiert, was unschön – um nicht zu sagen falsch – ist. Für die korrekte Sortierung entsprechend lokaler Gegebenheiten verwendet man am Einfachsten einen java.text.Collator
. Letzterer kann einfach als zweites Argument der comparing
-Methode übergeben werden.
Collator collator = Collator.getInstance();
movies.sort(comparing(Movie::getTitle, collator));
Im nächsten Beispiel werden die Filme aufsteigend nach Preis sortiert. Dabei ist zu beachten, dass mehrere viele Filme den gleichen Preis haben können. Aus diesem Grund wird als zweites Ordnungskriterium der Filmtitel verwendet. Java 8 unterstützt diese Art der Sortierung mit der Default-Methode Comparator.thenComparing
. Das folgende Listing zeigt den vollständigen Aufruf.
movies.sort(comparing(Movie::getRentalPrice)
.thenComparing(Movie::getTitle, collator));
Seit jeher tut sich Java schwer mit dem Dualismus zwischen primitiven Typen und Klassen. Die Einführung des sogenannten Auto-Boxing hat die Schmerzen zwar teilweise gelindert. Jedoch kann dieser Automatismus auf kritischen Pfaden zu Performance-Problemen führen. Java 8 setzt daher an vielen Stellen auf Spezialisierungen für primitive Datentypen. Im folgenden Listing ist ein solcher Fall für die Sortierung nach Jahr der Veröffentlichung zu sehen.
movies.sort(comparingInt(Movie::getReleaseYear)
.thenComparing(Movie::getTitle, collator));
Um die neuesten Filme zuerst zu sehen, muss lediglich an der richtigen Stelle ein Aufruf der Default-Methode Comparator.reversed
eingefügt werden.
movies.sort(comparingInt(Movie::getReleaseYear).reversed()
.thenComparing(Movie::getTitle, collator));
Das letzte Beispiel betrachtet noch den Umgang mit null
-Referenzen. Die vorherigen Beispiele würden eine NullPointerException
werfen, wenn eines der zu vergleichenden Attribute null
wäre. Java 8 bietet jedoch mit den beiden statischen Methoden Comparator.nullsFirst
und Comparator.nullsLast
die Möglichkeit, einen existierenden Comparator
tolerant gegenüber null
-Referenzen zu machen, und sie entweder nach oben oder nach unten zu sortieren. Davon wird im folgenden Listing Gebrauch gemacht, um die Filme nach dem optionalen Genre zu sortieren.
movies.sort(comparing(Movie::getGenre, nullsLast(Genre::compareTo))
.thenComparing(Movie::getTitle, collator));
Insgesamt wird die Comparator
-Implementierung mit Java 8 durch Lambda-Ausdrücke, Methoden-Referenzen sowie zahlreiche statische und Default-Methoden der Schnittstelle Comparator
deutlich einfacher. Aus diesem Grund ist die Sortierung auch ein gutes Beispiel um zu vermitteln, dass die Spracherweiterungen nicht nur die Stream-API unterstützen. Diese Features sind längst überfällig und werden die Entwicklung in Java nachhaltig verändern.