Kapitel 2: Nutzung von Sammlungen
Wir verwenden häufig verschiedene Sammlungen, Zahlen, Zeichenfolgen und Objekte. Sie sind überall, und selbst wenn der Code, der die Sammlung betreibt, leicht optimiert werden kann, wird der Code dadurch viel klarer. In diesem Kapitel untersuchen wir, wie man Lambda-Ausdrücke zum Bearbeiten von Sammlungen verwendet. Wir verwenden es, um Sammlungen zu durchlaufen, Sammlungen in neue Sammlungen umzuwandeln, Elemente aus Sammlungen zu entfernen und Sammlungen zusammenzuführen.
Durchlaufen Sie die Liste
Das Durchlaufen einer Liste ist die grundlegendste Mengenoperation, und ihre Operationen haben im Laufe der Jahre einige Änderungen erfahren. Wir verwenden ein kleines Beispiel für das Durchqueren von Namen und stellen es von der ältesten Version bis zur elegantesten Version von heute vor.
Mit dem folgenden Code können wir ganz einfach eine unveränderliche Namensliste erstellen:
Kopieren Sie den Codecode wie folgt:
final List<String> friends =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
System.out.println(friends.get(i));
}
Das Folgende ist die gebräuchlichste Methode zum Durchlaufen einer Liste und zum Drucken, obwohl sie auch die allgemeinste ist:
Kopieren Sie den Codecode wie folgt:
for(int i = 0; i < friends.size(); i++) {
System.out.println(friends.get(i));
}
Ich nenne diese Schreibweise masochistisch – sie ist ausführlich und fehleranfällig. Wir müssen innehalten und darüber nachdenken: „Ist es i< oder i<=?“ Das macht nur dann Sinn, wenn wir ein bestimmtes Element bearbeiten müssen, aber selbst dann können wir immer noch funktionale Ausdrücke verwenden, die dem Prinzip folgen Unveränderlichkeitsstil, den wir gleich besprechen werden.
Java bietet auch eine relativ fortschrittliche Struktur.
Kopieren Sie den Codecode wie folgt:
Sammlungen/fpij/Iteration.java
for(String name : friends) {
System.out.println(name);
}
Unter der Haube wird die Iteration auf diese Weise mithilfe der Iterator-Schnittstelle implementiert, indem die Methoden hasNext und next aufgerufen werden. Bei beiden Methoden handelt es sich um externe Iteratoren, die die Vorgehensweise mit dem kombinieren, was Sie tun möchten. Wir steuern die Iteration explizit und sagen ihr, wo sie beginnen und wo sie enden soll; die zweite Version erledigt dies unter der Haube durch die Iterator-Methode. Bei expliziten Operationen können Sie auch break- und continue-Anweisungen verwenden, um die Iteration zu steuern. In der zweiten Version fehlen einige Dinge im Vergleich zur ersten. Dieser Ansatz ist besser als der erste, wenn wir nicht beabsichtigen, ein Element der Sammlung zu ändern. Beide Methoden sind jedoch zwingend erforderlich und sollten im aktuellen Java aufgegeben werden. Es gibt mehrere Gründe für den Wechsel zum funktionalen Stil:
1. Die for-Schleife selbst ist seriell und schwer zu parallelisieren.
2. Eine solche Schleife ist nicht polymorph; Sie erhalten das, was Sie verlangen. Wir übergeben die Sammlung direkt an die for-Schleife, anstatt eine Methode (die Polymorphismus unterstützt) für die Sammlung aufzurufen, um eine bestimmte Operation auszuführen.
3. Aus gestalterischer Sicht verstößt ein auf diese Weise geschriebener Code gegen das „Tell, Don't Ask“-Prinzip. Wir fordern, dass eine Iteration durchgeführt wird, anstatt die Iteration der zugrunde liegenden Bibliothek zu überlassen.
Es ist Zeit, von der alten imperativen Programmierung zur eleganteren funktionalen Programmierung interner Iteratoren zu wechseln. Nachdem wir interne Iteratoren verwendet haben, überlassen wir viele spezifische Vorgänge zur Ausführung der zugrunde liegenden Methodenbibliothek, sodass Sie sich stärker auf spezifische Geschäftsanforderungen konzentrieren können. Die zugrunde liegende Funktion ist für die Iteration verantwortlich. Wir verwenden zunächst einen internen Iterator, um die Namensliste aufzuzählen.
Die Iterable-Schnittstelle wurde in JDK8 erweitert. Sie hat einen speziellen Namen namens forEach, der einen Parameter vom Typ Comsumer empfängt. Wie der Name schon sagt, konsumiert eine Consumer-Instanz das Objekt, das ihr über ihre Accept-Methode übergeben wird. Wir verwenden die bekannte Syntax anonymer innerer Klassen, um die forEach-Methode zu verwenden:
Kopieren Sie den Codecode wie folgt:
friends.forEach(new Consumer<String>() { public void Accept(final String name) {
System.out.println(name);
});
Wir haben die forEach-Methode für die Friends-Sammlung aufgerufen und ihr eine anonyme Implementierung von Consumer übergeben. Diese forEach-Methode ruft für jedes Element in der Sammlung die Accept-Methode des übergebenen Consumers auf und ermöglicht so die Verarbeitung dieses Elements. In diesem Beispiel geben wir einfach seinen Wert aus, also den Namen. Schauen wir uns die Ausgabe dieser Version an, die mit der der beiden vorherigen identisch ist:
Kopieren Sie den Codecode wie folgt:
Brian
Nate
Neal
Raju
Sara
Scott
Wir haben nur eines geändert: Wir haben die veraltete for-Schleife über Bord geworfen und einen neuen internen Iterator verwendet. Der Vorteil besteht darin, dass wir nicht angeben müssen, wie die Sammlung iteriert werden soll, und uns mehr auf die Verarbeitung jedes Elements konzentrieren können. Der Nachteil ist, dass der Code ausführlicher aussieht – was die Freude am neuen Codierungsstil fast zunichte macht. Glücklicherweise lässt sich dies leicht ändern, und hier kommt die Leistungsfähigkeit von Lambda-Ausdrücken und neuen Compilern ins Spiel. Nehmen wir noch eine Änderung vor und ersetzen die anonyme innere Klasse durch einen Lambda-Ausdruck.
Kopieren Sie den Codecode wie folgt:
friends.forEach((final String name) -> System.out.println(name));
So sieht es viel besser aus. Es gibt weniger Code, aber schauen wir uns zunächst an, was das bedeutet. Die forEach-Methode ist eine Funktion höherer Ordnung, die einen Lambda-Ausdruck oder Codeblock empfängt, um die Elemente in der Liste zu bearbeiten. Bei jedem Aufruf werden die Elemente in der Sammlung an die Namensvariable gebunden. Die zugrunde liegende Bibliothek hostet die Aktivität des Lambda-Ausdrucksaufrufs. Es kann entscheiden, die Ausführung von Ausdrücken zu verzögern und gegebenenfalls parallele Berechnungen durchzuführen. Die Ausgabe dieser Version ist auch die gleiche wie die vorherige.
Kopieren Sie den Codecode wie folgt:
Brian
Nate
Neal
Raju
Sara
Scott
Die interne Iteratorversion ist prägnanter. Darüber hinaus können wir uns durch die Verwendung mehr auf die Verarbeitung jedes Elements konzentrieren, anstatt es zu durchlaufen – das ist deklarativ.
Allerdings weist diese Version Mängel auf. Sobald die forEach-Methode mit der Ausführung beginnt, können wir im Gegensatz zu den anderen beiden Versionen nicht aus dieser Iteration ausbrechen. (Natürlich gibt es auch andere Möglichkeiten, dies zu tun). Daher wird diese Schreibweise häufiger verwendet, wenn jedes Element in der Sammlung verarbeitet werden muss. Später werden wir einige andere Funktionen vorstellen, mit denen wir den Schleifenprozess steuern können.
Die Standardsyntax für Lambda-Ausdrücke besteht darin, die Parameter in () einzufügen, Typinformationen bereitzustellen und Kommas zum Trennen der Parameter zu verwenden. Um uns zu befreien, kann der Java-Compiler auch automatisch eine Typableitung durchführen. Natürlich ist es bequemer, keine Typen zu schreiben. Es gibt weniger Arbeit und die Welt ist ruhiger. Das Folgende ist die vorherige Version nach dem Entfernen des Parametertyps:
Kopieren Sie den Codecode wie folgt:
friends.forEach((name) -> System.out.println(name));
In diesem Beispiel weiß der Java-Compiler durch Kontextanalyse, dass der Namenstyp String ist. Es betrachtet die Signatur der aufgerufenen Methode forEach und analysiert dann die funktionale Schnittstelle in den Parametern. Anschließend wird die abstrakte Methode in dieser Schnittstelle analysiert und die Anzahl und Art der Parameter überprüft. Auch wenn dieser Lambda-Ausdruck mehrere Parameter empfängt, können wir dennoch eine Typableitung durchführen, aber in diesem Fall können nicht alle Parameter Parametertypen in Lambda-Ausdrücken haben, die Parametertypen müssen überhaupt geschrieben werden, oder wenn sie geschrieben werden, müssen sie geschrieben werden vollständig.
Der Java-Compiler behandelt Lambda-Ausdrücke mit einem einzelnen Parameter speziell: Wenn Sie eine Typinferenz durchführen möchten, können die Klammern um den Parameter weggelassen werden.
Kopieren Sie den Codecode wie folgt:
friends.forEach(name -> System.out.println(name));
Hier gibt es eine kleine Einschränkung: Die für die Typinferenz verwendeten Parameter sind nicht vom endgültigen Typ. Im vorherigen Beispiel der expliziten Deklaration eines Typs haben wir den Parameter auch als final markiert. Dadurch wird verhindert, dass Sie den Wert des Parameters im Lambda-Ausdruck ändern. Im Allgemeinen ist es eine schlechte Angewohnheit, den Wert eines Parameters zu ändern, was leicht zu Fehlern führen kann. Daher ist es eine gute Angewohnheit, ihn als endgültig zu markieren. Wenn wir Typinferenz verwenden möchten, müssen wir uns leider selbst an die Regeln halten und dürfen die Parameter nicht ändern, da der Compiler uns nicht mehr schützt.
Es hat viel Mühe gekostet, bis zu diesem Punkt zu gelangen, aber jetzt ist die Codemenge tatsächlich etwas kleiner. Aber das ist noch nicht das Einfachste. Probieren wir diese letzte minimalistische Version aus.
Kopieren Sie den Codecode wie folgt:
friends.forEach(System.out::println);
Im obigen Code verwenden wir eine Methodenreferenz. Wir können den gesamten Code direkt durch den Methodennamen ersetzen. Wir werden dies im nächsten Abschnitt ausführlich erläutern, aber erinnern wir uns zunächst an ein berühmtes Zitat von Antoine de Saint-Exupéry: Perfektion ist nicht das, was hinzugefügt werden kann, sondern das, was nicht mehr weggenommen werden kann.
Mit Lambda-Ausdrücken können wir Sammlungen präzise und klar durchqueren. Im nächsten Abschnitt werden wir darüber sprechen, wie wir so präzisen Code schreiben können, wenn wir Löschvorgänge und Sammlungskonvertierungen durchführen.