Verwendung von lexikalischem Geltungsbereich und Abschlüssen
Viele Entwickler haben dieses Missverständnis und glauben, dass die Verwendung von Lambda-Ausdrücken zu Coderedundanz führt und die Codequalität verringert. Im Gegenteil: Egal wie komplex der Code wird, wir werden der Einfachheit halber keine Kompromisse bei der Codequalität eingehen, wie wir weiter unten sehen werden.
Wir konnten den Lambda-Ausdruck im vorherigen Beispiel wiederverwenden; wenn wir jedoch einen anderen Buchstaben finden, tritt das Problem der Code-Redundanz schnell wieder auf. Lassen Sie uns dieses Problem zunächst weiter analysieren und dann den lexikalischen Bereich und Abschlüsse verwenden, um es zu lösen.
Durch Lambda-Ausdrücke verursachte Redundanz
Lassen Sie uns die Buchstaben von Freunden herausfiltern, die mit N oder B beginnen. Wenn wir mit dem obigen Beispiel fortfahren, könnte der Code, den wir schreiben, so aussehen:
Kopieren Sie den Codecode wie folgt:
final Predicate<String> getsWithN = name -> name.startsWith("N");
final Predicate<String> getsWithB = name -> name.startsWith("B");
final long countFriendsStartN =
friends.stream()
.filter(startsWithN).count();
final long countFriendsStartB =
friends.stream()
.filter(startsWithB).count();
Das erste Prädikat bestimmt, ob der Name mit N beginnt, und das zweite bestimmt, ob der Name mit B beginnt. Wir übergeben diese beiden Instanzen jeweils an zwei Filtermethodenaufrufe. Das scheint vernünftig, aber die beiden Prädikate sind redundant, es handelt sich lediglich um unterschiedliche Buchstaben im Scheck. Mal sehen, wie wir diese Redundanz vermeiden können.
Verwenden Sie den lexikalischen Bereich, um Redundanz zu vermeiden
In der ersten Lösung können wir die Buchstaben als Parameter der Funktion extrahieren und diese Funktion an die Filtermethode übergeben. Dies ist eine gute Methode, aber der Filter wird nicht von allen Funktionen akzeptiert. Es akzeptiert nur Funktionen mit nur einem Parameter, der dem Element in der Sammlung entspricht und einen booleschen Wert zurückgibt. Es wird erwartet, dass es sich bei dem übergebenen Wert um ein Prädikat handelt.
Wir hoffen, dass es einen Ort gibt, an dem dieser Buchstabe zwischengespeichert werden kann, bis der Parameter übergeben wird (in diesem Fall der Namensparameter). Lassen Sie uns eine neue Funktion wie diese erstellen.
Kopieren Sie den Codecode wie folgt:
public static Predicate<String> checkIfStartsWith(final String letter) {
Rückgabename -> name.startsWith(letter);
}
Wir haben eine statische Funktion checkIfStartsWith definiert, die einen String-Parameter empfängt und ein Predicate-Objekt zurückgibt, das zur späteren Verwendung an die Filtermethode übergeben werden kann. Im Gegensatz zu den Funktionen höherer Ordnung, die wir zuvor gesehen haben und die Funktionen als Parameter akzeptieren, gibt diese Methode eine Funktion zurück. Es handelt sich aber auch um eine Funktion höherer Ordnung, die wir bereits in Evolution, not change, auf Seite 12 erwähnt haben.
Das von der checkIfStartsWith-Methode zurückgegebene Predicate-Objekt unterscheidet sich etwas von anderen Lambda-Ausdrücken. In der Anweisung return name -> name.startsWith(letter) wissen wir genau, was name ist, es ist der Parameter, der an den Lambda-Ausdruck übergeben wird. Aber was genau ist der variable Buchstabe? Es liegt außerhalb der Domäne der anonymen Funktion. Java findet die Domäne, in der der Lambda-Ausdruck definiert ist, und entdeckt den Variablenbuchstaben. Dies wird als lexikalischer Geltungsbereich bezeichnet. Der lexikalische Bereich ist eine sehr nützliche Sache. Er ermöglicht es uns, eine Variable in einem Bereich zwischenzuspeichern, um sie später in einem anderen Kontext zu verwenden. Da dieser Lambda-Ausdruck in seinem Gültigkeitsbereich Variablen verwendet, wird diese Situation auch als Abschluss bezeichnet. Können Sie bezüglich der Zugriffsbeschränkungen des lexikalischen Bereichs die Einschränkungen des lexikalischen Bereichs auf Seite 31 lesen?
Gibt es Einschränkungen hinsichtlich des lexikalischen Umfangs?
In einem Lambda-Ausdruck können wir nur auf endgültige Typen in seinem Gültigkeitsbereich oder tatsächlich auf lokale Variablen des endgültigen Typs zugreifen.
Der Lambda-Ausdruck kann sofort, verzögert oder von einem anderen Thread aufgerufen werden. Um Rassenkonflikte zu vermeiden, dürfen die lokalen Variablen in der Domäne, auf die wir zugreifen, nach der Initialisierung nicht geändert werden. Jeder Änderungsvorgang führt zu einer Kompilierungsausnahme.
Das Markieren als „final“ löst dieses Problem, aber Java zwingt uns nicht dazu, es auf diese Weise zu markieren. Tatsächlich betrachtet Java zwei Dinge. Zum einen muss die Variable, auf die zugegriffen wird, in der Methode, in der sie definiert ist, und vor der Definition des Lambda-Ausdrucks initialisiert werden. Zweitens können die Werte dieser Variablen nicht geändert werden – das heißt, sie sind tatsächlich vom Typ final, obwohl sie nicht als solche gekennzeichnet sind.
Zustandslose Lambda-Ausdrücke sind Laufzeitkonstanten, während diejenigen, die lokale Variablen verwenden, zusätzlichen Rechenaufwand verursachen.
Beim Aufruf der Filtermethode können wir den von der checkIfStartsWith-Methode zurückgegebenen Lambda-Ausdruck wie folgt verwenden:
Kopieren Sie den Codecode wie folgt:
final long countFriendsStartN =
friends.stream() .filter(checkIfStartsWith("N")).count();
final long countFriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();
Bevor wir die Filtermethode aufrufen, haben wir zunächst die Methode checkIfStartsWith() aufgerufen und die gewünschten Buchstaben übergeben. Dieser Aufruf gibt schnell einen Lambda-Ausdruck zurück, den wir dann an die Filtermethode übergeben.
Durch die Erstellung einer Funktion höherer Ordnung (in diesem Fall checkIfStartsWith) und die Verwendung des lexikalischen Bereichs konnten wir Redundanz erfolgreich aus dem Code entfernen. Wir müssen nicht mehr wiederholt feststellen, ob der Name mit einem bestimmten Buchstaben beginnt.
Umgestalten, Umfang reduzieren
Im vorherigen Beispiel haben wir eine statische Methode verwendet, aber wir möchten die statische Methode nicht zum Zwischenspeichern von Variablen verwenden, da dies unseren Code durcheinander bringen würde. Am besten beschränken Sie den Umfang dieser Funktion auf den Ort, an dem sie verwendet wird. Um dies zu erreichen, können wir eine Funktionsschnittstelle verwenden.
Kopieren Sie den Codecode wie folgt:
final Function<String, Predicate<String>> getsWithLetter = (String-Buchstabe) -> {
Predicate<String> checkStarts = (String name) -> name.startsWith(letter);
return checkStarts; };
Dieser Lambda-Ausdruck ersetzt die ursprüngliche statische Methode. Er kann in eine Funktion eingefügt und definiert werden, bevor er benötigt wird. Die Variable startWithLetter bezieht sich auf eine Funktion, deren Eingabeparameter String und deren Ausgabeparameter Predicate ist.
Im Vergleich zur Verwendung der statischen Methode ist diese Version viel einfacher, wir können sie jedoch weiter umgestalten, um sie prägnanter zu gestalten. Aus praktischer Sicht ist diese Funktion dieselbe wie die vorherige statische Methode; beide empfangen einen String und geben ein Prädikat zurück. Anstatt ein Prädikat explizit zu deklarieren, ersetzen wir es vollständig durch einen Lambda-Ausdruck.
Kopieren Sie den Codecode wie folgt:
final Function<String, Predicate<String>> getsWithLetter = (String-Buchstabe) -> (String-Name) -> name.startsWith(letter);
Wir haben das Durcheinander beseitigt, aber wir können auch die Typdeklaration entfernen, um sie prägnanter zu gestalten, und der Java-Compiler führt die Typableitung basierend auf dem Kontext durch. Werfen wir einen Blick auf die verbesserte Version.
Kopieren Sie den Codecode wie folgt:
final Function<String, Predicate<String>> getsWithLetter =
Buchstabe -> Name -> name.startsWith(letter);
Die Anpassung an diese prägnante Syntax erfordert einige Anstrengungen. Wenn es Sie blendet, schauen Sie sich zuerst woanders um. Wir haben die Umgestaltung des Codes abgeschlossen und können ihn nun verwenden, um die ursprüngliche checkIfStartsWith()-Methode wie folgt zu ersetzen:
Kopieren Sie den Codecode wie folgt:
final long countFriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
final long countFriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();
In diesem Abschnitt verwenden wir Funktionen höherer Ordnung. Wir haben gesehen, wie man Funktionen innerhalb von Funktionen erstellt, wenn wir eine Funktion an eine andere Funktion übergeben, und wie man eine Funktion von einer Funktion zurückgibt. Diese Beispiele zeigen alle die Einfachheit und Wiederverwendbarkeit, die Lambda-Ausdrücke bieten.
In diesem Abschnitt haben wir die Funktionen von Funktion und Prädikat vollständig ausgenutzt, aber werfen wir einen Blick auf den Unterschied zwischen ihnen. Prädikat akzeptiert einen Parameter vom Typ T und gibt einen booleschen Wert zurück, der den wahren oder falschen Wert der entsprechenden Beurteilungsbedingung darstellt. Wenn wir bedingte Urteile fällen müssen, können wir diese mit Predicateg vervollständigen. Methoden wie Filter, die Elemente filtern, erhalten Predicate als Parameter. Funciton stellt eine Funktion dar, deren Eingabeparameter Variablen vom Typ T sind und die ein Ergebnis vom Typ R zurückgibt. Es ist allgemeiner als Predicate, das nur boolesche Werte zurückgeben kann. Solange die Eingabe in eine Ausgabe konvertiert wird, können wir Function verwenden, daher ist es für Map sinnvoll, Function als Parameter zu verwenden.
Wie Sie sehen, ist die Auswahl von Elementen aus einer Sammlung sehr einfach. Im Folgenden stellen wir vor, wie Sie nur ein Element aus der Sammlung auswählen.