Implementieren Sie die Comparator-Schnittstelle
Die Comparator-Schnittstelle ist überall in der JDK-Bibliothek zu sehen, von der Suche über die Sortierung bis hin zu Umkehroperationen und so weiter. In Java 8 wird es zu einer funktionalen Schnittstelle. Der Vorteil besteht darin, dass wir den Komparator mithilfe der Streaming-Syntax implementieren können.
Lassen Sie uns Comparator auf verschiedene Arten implementieren, um den Wert der neuen Syntax zu sehen. Ihre Finger werden es Ihnen danken. Da Sie keine anonymen inneren Klassen implementieren müssen, ersparen Sie sich viele Tastenanschläge.
Sortieren mit Komparator
Im folgenden Beispiel werden verschiedene Vergleichsmethoden verwendet, um eine Gruppe von Personen zu sortieren. Lassen Sie uns zunächst eine Person JavaBean erstellen.
Kopieren Sie den Codecode wie folgt:
öffentliche Klasse Person {
privater finaler String-Name;
privates Abschlussalter;
öffentliche Person(final String theName, final int theAge) {
name = theName;
Alter = theAge;
}
public String getName() { return name;
public int getAge() { return age;
public int ageDifference(final Person other) {
Rückkehralter - other.age;
}
öffentlicher String toString() {
return String.format("%s - %d", Name, Alter);
}
}
Wir können die Comparator-Schnittstelle über die Person-Klasse implementieren, auf diese Weise können wir jedoch nur eine Vergleichsmethode verwenden. Wir möchten verschiedene Attribute vergleichen können – etwa Name, Alter oder eine Kombination davon. Um den Vergleich flexibel durchzuführen, können wir Comparator verwenden, um relevanten Code zu generieren, wenn wir vergleichen müssen.
Erstellen wir zunächst eine Liste von Personen mit jeweils unterschiedlichem Namen und Alter.
Kopieren Sie den Codecode wie folgt:
final List<Person> people = Arrays.asList(
neue Person("John", 20),
neue Person("Sara", 21),
neue Person("Jane", 21),
neue Person("Greg", 35));
Wir können Personen in aufsteigender oder absteigender Reihenfolge nach ihrem Namen oder Alter sortieren. Die allgemeine Methode besteht darin, anonyme innere Klassen zu verwenden, um die Comparator-Schnittstelle zu implementieren. Wenn es auf diese Weise geschrieben wird, ist nur der relevantere Code sinnvoll und der Rest ist nur eine Formsache. Die Verwendung von Lambda-Ausdrücken kann sich auf das Wesentliche des Vergleichs konzentrieren.
Sortieren wir sie zunächst nach Alter, vom jüngsten zum ältesten.
Da wir nun ein List-Objekt haben, können wir dessen sort()-Methode zum Sortieren verwenden. Allerdings hat diese Methode auch ihre Probleme. Dies ist eine void-Methode, was bedeutet, dass sich die Liste ändert, wenn wir diese Methode aufrufen. Um die ursprüngliche Liste beizubehalten, müssen wir zunächst eine Kopie erstellen und dann die Methode sort() aufrufen. Es war einfach zu viel Aufwand. Zu diesem Zeitpunkt müssen wir uns an die Stream-Klasse wenden, um Hilfe zu erhalten.
Wir können ein Stream-Objekt aus der Liste abrufen und dann seine sorted()-Methode aufrufen. Es gibt eine sortierte Sammlung zurück, anstatt die ursprüngliche Sammlung zu ändern. Mit dieser Methode können Sie die Parameter von Comparator einfach konfigurieren.
Kopieren Sie den Codecode wie folgt:
List<Person> aufsteigendes Alter =
people.stream()
.sorted((person1, person2) -> person1.ageDifference(person2))
.collect(toList());
printPeople("Sortiert in aufsteigender Reihenfolge nach Alter: ", aufsteigendes Alter);
Wir konvertieren die Liste zunächst über die Methode stream() in ein Stream-Objekt. Rufen Sie dann die Methode sorted() auf. Diese Methode akzeptiert einen Comparator-Parameter. Da Comparator eine funktionale Schnittstelle ist, können wir einen Lambda-Ausdruck übergeben. Schließlich rufen wir die Methode „collect“ auf und lassen sie die Ergebnisse in einer Liste speichern. Die Collect-Methode ist ein Reduzierer, der die Objekte während des Iterationsprozesses in ein bestimmtes Format oder einen bestimmten Typ ausgeben kann. Die toList()-Methode ist eine statische Methode der Collectors-Klasse.
Die abstrakte Methode CompareTo() von Comparator empfängt zwei Parameter, die zu vergleichenden Objekte, und gibt ein Ergebnis vom Typ int zurück. Um damit kompatibel zu sein, erhält unser Lambda-Ausdruck auch zwei Parameter, zwei Person-Objekte, deren Typen vom Compiler automatisch abgeleitet werden. Wir geben einen int-Typ zurück, der angibt, ob die verglichenen Objekte gleich sind.
Da wir nach Alter sortieren möchten, vergleichen wir das Alter der beiden Objekte und geben dann das Ergebnis des Vergleichs zurück. Wenn sie die gleiche Größe haben, wird 0 zurückgegeben. Andernfalls wird eine negative Zahl zurückgegeben, wenn die erste Person jünger ist, und eine positive Zahl, wenn die erste Person älter ist.
Die Methode sorted() durchläuft jedes Element der Zielsammlung und ruft den angegebenen Komparator auf, um die Sortierreihenfolge der Elemente zu bestimmen. Die Ausführungsmethode der Methode sorted() ähnelt in gewisser Weise der zuvor erwähnten Methode Reduce(). Die Methode Reduce() reduziert die Liste schrittweise auf ein Ergebnis. Die Methode sorted() sortiert nach den Vergleichsergebnissen.
Sobald wir die Ergebnisse sortiert haben, rufen wir eine printPeople()-Methode auf.
Kopieren Sie den Codecode wie folgt:
öffentliches statisches void printPeople(
final String message, final List<Person> people) {
System.out.println(message);
people.forEach(System.out::println);
}
Bei dieser Methode drucken wir zunächst eine Nachricht, durchlaufen dann die Liste und drucken jedes darin enthaltene Element aus.
Rufen wir die Methode sorted() auf, um zu sehen, wie sie die Personen in der Liste vom Jüngsten zum Ältesten sortiert.
Kopieren Sie den Codecode wie folgt:
Aufsteigend nach Alter sortiert:
Johannes – 20
Sara – 21
Jane – 21
Gregor – 35
Schauen wir uns noch einmal die Methode sorted() an, um eine Verbesserung vorzunehmen.
Kopieren Sie den Codecode wie folgt:
.sorted((person1, person2) -> person1.ageDifference(person2))
Im übergebenen Lambda-Ausdruck leiten wir einfach diese beiden Parameter weiter – der erste Parameter wird als Aufrufziel der ageDifference()-Methode und der zweite Parameter als ihr Parameter verwendet. Aber wir können es nicht so schreiben, sondern einen Office-Space-Modus verwenden, das heißt, Methodenreferenzen verwenden und das Routing dem Java-Compiler überlassen.
Das hier verwendete Parameter-Routing unterscheidet sich ein wenig von dem, was wir zuvor gesehen haben. Wir haben bereits gesehen, dass Argumente entweder als Aufrufziele oder als Aufrufparameter übergeben werden. Jetzt haben wir zwei Parameter und möchten sie in zwei Teile aufteilen, einen als Ziel des Methodenaufrufs und den zweiten als Parameter. Machen Sie sich keine Sorgen, der Java-Compiler wird Ihnen sagen: „Ich kümmere mich darum.“
Wir können den Lambda-Ausdruck in der vorherigen sorted()-Methode durch eine kurze und prägnante ageDifference-Methode ersetzen.
Kopieren Sie den Codecode wie folgt:
people.stream()
.sorted(Person::ageDifference)
Dieser Code ist dank der vom Java-Compiler bereitgestellten Methodenreferenzen sehr prägnant. Der Compiler empfängt zwei Personeninstanzparameter und verwendet den ersten als Ziel der ageDifference()-Methode und den zweiten als Methodenparameter. Wir überlassen diese Arbeit dem Compiler, anstatt den Code direkt zu schreiben. Bei Verwendung dieser Methode müssen wir sicherstellen, dass der erste Parameter das aufrufende Ziel der referenzierten Methode und der verbleibende Parameter der Eingabeparameter der Methode ist.
Komparator wiederverwenden
Es ist einfach, die Personen in der Liste vom Jüngsten zum Ältesten zu sortieren, und es ist auch einfach, die Personen vom Ältesten zum Jüngsten zu sortieren. Lass es uns versuchen.
Kopieren Sie den Codecode wie folgt:
printPeople("Sortiert in absteigender Reihenfolge nach Alter:",
people.stream()
.sorted((person1, person2) -> person2.ageDifference(person1))
.collect(toList()));
Wir rufen die Methode sorted() auf und übergeben einen Lambda-Ausdruck, der wie im vorherigen Beispiel in die Comparator-Schnittstelle passt. Der einzige Unterschied besteht in der Implementierung dieses Lambda-Ausdrucks – wir haben die Reihenfolge der zu vergleichenden Personen geändert. Die Ergebnisse sollten nach ihrem Alter vom Ältesten zum Jüngsten geordnet werden. Werfen wir einen Blick darauf.
Kopieren Sie den Codecode wie folgt:
Absteigend nach Alter sortiert:
Gregor – 35
Sara – 21
Jane – 21
Johannes – 20
Es erfordert nicht viel Aufwand, nur die Vergleichslogik zu ändern. Wir können diese Version jedoch nicht in eine Methodenreferenz umwandeln, da die Reihenfolge der Parameter nicht den Regeln des Parameterroutings für Methodenreferenzen entspricht. Der erste Parameter wird nicht als aufrufendes Ziel der Methode, sondern als Methodenparameter verwendet. Es gibt eine Möglichkeit, dieses Problem zu lösen, die auch Doppelarbeit reduziert. Mal sehen, wie es geht.
Wir haben zuvor zwei Lambda-Ausdrücke erstellt: Der eine dient zum Sortieren nach Alter von klein nach groß und der andere zum Sortieren von groß nach klein. Dies führt zu Coderedundanz und -duplizierung und verstößt gegen das DRY-Prinzip. Wenn wir nur die Sortierreihenfolge anpassen möchten, stellt JDK eine umgekehrte Methode bereit, die standardmäßig über einen speziellen Methodenmodifikator verfügt. Wir werden es in der Standardmethode auf Seite 77 besprechen. Hier verwenden wir zunächst die Methode reversed(), um Redundanz zu entfernen.
Kopieren Sie den Codecode wie folgt:
Comparator<Person> vergleichenAscending =
(person1, person2) -> person1.ageDifference(person2);
Comparator<Person> vergleichenDescending = vergleichenAscending.reversed();
Wir haben zunächst einen Komparator namens „compareAscending“ erstellt, um Personen nach Alter vom Jüngsten zum Ältesten zu sortieren. Um die Vergleichsreihenfolge umzukehren, müssen wir diesen Code nicht erneut schreiben, sondern nur die Methode reversed() des ersten Komparators aufrufen, um das zweite Komparatorobjekt zu erhalten. Unter der Haube der Methode reversed() wird ein Komparator erstellt, um die Reihenfolge der verglichenen Parameter umzukehren. Dies zeigt, dass reversed auch eine Methode höherer Ordnung ist – sie erstellt eine Funktion und gibt sie ohne Nebenwirkungen zurück. Wir verwenden diese beiden Komparatoren im Code.
Kopieren Sie den Codecode wie folgt:
printPeople("Sortiert in aufsteigender Reihenfolge nach Alter: ",
people.stream()
.sorted(compareAscending)
.collect(toList())
);
printPeople("Sortiert in absteigender Reihenfolge nach Alter:",
people.stream()
.sorted(compareDescending)
.collect(toList())
);
Aus dem Code ist deutlich ersichtlich, dass diese neuen Funktionen von Java8 die Redundanz und Komplexität des Codes erheblich reduziert haben, aber die Vorteile gehen weit über diese hinaus. Im JDK warten unzählige Möglichkeiten darauf, von Ihnen erkundet zu werden.
Wir können bereits nach Alter sortieren, auch die Sortierung nach Namen ist einfach. Sortieren wir sie lexikografisch nach Namen. Ebenso müssen wir nur die Logik im Lambda-Ausdruck ändern.
Kopieren Sie den Codecode wie folgt:
printPeople("Aufsteigend nach Namen sortiert: ",
people.stream()
.sorted((person1, person2) ->
person1.getName().compareTo(person2.getName()))
.collect(toList()));
Die Ausgabeergebnisse werden in lexikografischer Reihenfolge nach Namen sortiert.
Kopieren Sie den Codecode wie folgt:
Aufsteigend nach Namen sortiert:
Gregor – 35
Jane – 21
Johannes – 20
Sara – 21
Bisher haben wir entweder nach Alter oder nach Namen sortiert. Wir können die Logik von Lambda-Ausdrücken intelligenter gestalten. Beispielsweise können wir gleichzeitig nach Alter und Namen sortieren.
Wählen wir die jüngste Person auf der Liste aus. Wir können zunächst nach Alter vom kleinsten zum größten sortieren und dann das erste in den Ergebnissen auswählen. Aber das funktioniert eigentlich nicht. Stream verfügt über eine min()-Methode, um dies zu erreichen. Diese Methode akzeptiert auch einen Comparator, gibt jedoch das kleinste Objekt in der Sammlung zurück. Nutzen wir es.
Kopieren Sie den Codecode wie folgt:
people.stream()
.min(Person::ageDifference)
.ifPresent(jüngste -> System.out.println("Jüngste: " + jüngste));
Beim Aufruf der Methode min() haben wir die Methodenreferenz ageDifference verwendet. Die Methode min() gibt ein optionales Objekt zurück, da die Liste möglicherweise leer ist und möglicherweise mehr als eine jüngste Person darin enthalten ist. Dann holen wir uns die jüngste Person über die ifPrsend()-Methode von Optinal und drucken ihre detaillierten Informationen aus. Werfen wir einen Blick auf die Ausgabe.
Kopieren Sie den Codecode wie folgt:
Jüngster: John – 20
Auch der Export des ältesten Exemplars ist sehr einfach. Übergeben Sie einfach diese Methodenreferenz an eine max()-Methode.
Kopieren Sie den Codecode wie folgt:
people.stream()
.max(Person::ageDifference)
.ifPresent(eldest -> System.out.println("Eldest: " + eldest));
Werfen wir einen Blick auf den Namen und das Alter des Ältesten.
Kopieren Sie den Codecode wie folgt:
Ältester: Greg – 35
Mit Lambda-Ausdrücken und Methodenreferenzen wird die Implementierung von Komparatoren einfacher und komfortabler. JDK führt außerdem viele praktische Methoden in die Compararor-Klasse ein, die uns einen reibungsloseren Vergleich ermöglichen, wie wir weiter unten sehen werden.
Mehrfachvergleiche und Streaming-Vergleiche
Werfen wir einen Blick auf die praktischen neuen Methoden der Comparator-Schnittstelle und nutzen sie zum Vergleichen mehrerer Eigenschaften.
Fahren wir mit dem Beispiel aus dem vorherigen Abschnitt fort. Nach Namen sortiert, das haben wir oben geschrieben:
Kopieren Sie den Codecode wie folgt:
people.stream()
.sorted((person1, person2) ->
person1.getName().compareTo(person2.getName()));
Verglichen mit der Schreibmethode der inneren Klasse des letzten Jahrhunderts ist diese Schreibmethode einfach zu einfach. Es kann jedoch einfacher gemacht werden, wenn wir einige Funktionen in der Comparator-Klasse verwenden. Die Verwendung dieser Funktionen kann es uns ermöglichen, unseren Zweck reibungsloser auszudrücken. Wenn wir beispielsweise nach Namen sortieren möchten, können wir schreiben:
Kopieren Sie den Codecode wie folgt:
final Function<Person, String> byName = person -> person.getName();
people.stream()
.sorted(comparing(byName));
In diesem Code haben wir die statische Methode vergleichen() der Klasse Comparator importiert. Die Methode „Vergleichen()“ verwendet den übergebenen Lambda-Ausdruck, um ein Comparator-Objekt zu generieren. Mit anderen Worten handelt es sich auch um eine Funktion höherer Ordnung, die eine Funktion als Eingabeparameter akzeptiert und eine andere Funktion zurückgibt. Ein solcher Code macht nicht nur die Syntax prägnanter, sondern kann auch das eigentliche Problem, das wir lösen möchten, besser ausdrücken.
Damit können Mehrfachvergleiche reibungsloser gestaltet werden. Der folgende Code zum Beispiel, der nach Name und Alter vergleicht, sagt alles:
Kopieren Sie den Codecode wie folgt:
final Function<Person, Integer> byAge = person -> person.getAge();
final Function<Person, String> byTheirName = person -> person.getName();
printPeople("Sortiert in aufsteigender Reihenfolge nach Alter und Name: ",
people.stream()
.sorted(comparing(byAge).thenComparing(byTheirName))
.collect(toList()));
Wir haben zunächst zwei Lambda-Ausdrücke erstellt, einer gibt das Alter der angegebenen Person zurück und der andere gibt ihren Namen zurück. Beim Aufruf der Methode sorted() kombinieren wir diese beiden Ausdrücke, sodass mehrere Attribute verglichen werden können. Die Methode „Vergleichen“ erstellt einen Vergleicher basierend auf dem Alter und gibt ihn zurück. Anschließend rufen wir die Methode „thenComparing()“ für den zurückgegebenen Vergleicher auf, um einen kombinierten Vergleicher zu erstellen, der Alter und Name vergleicht. Die folgende Ausgabe ist das Ergebnis der Sortierung zuerst nach Alter und dann nach Name.
Kopieren Sie den Codecode wie folgt:
Aufsteigend nach Alter und Name sortiert:
Johannes – 20
Jane – 21
Sara – 21
Gregor – 35
Wie Sie sehen, lässt sich die Implementierung von Comparator problemlos mit Lambda-Ausdrücken und den neuen Toolklassen des JDK kombinieren. Im Folgenden stellen wir Ihnen Sammler vor.