Kapitel 1 Hallo, Lambda-Ausdruck!
Abschnitt 1
Der Codierungsstil von Java steht vor gewaltigen Veränderungen.
Unsere tägliche Arbeit wird einfacher, bequemer und ausdrucksvoller. Java, eine neue Programmiermethode, ist schon vor Jahrzehnten in anderen Programmiersprachen aufgetaucht. Nachdem diese neuen Funktionen in Java eingeführt wurden, können wir Code schreiben, der prägnanter, eleganter und ausdrucksvoller ist und weniger Fehler aufweist. Wir können verschiedene Strategien und Entwurfsmuster mit weniger Code implementieren.
In diesem Buch werden wir die Programmierung im funktionalen Stil anhand von Beispielen aus der alltäglichen Programmierung untersuchen. Bevor wir diese neue und elegante Art des Entwerfens und Codierens verwenden, werfen wir zunächst einen Blick darauf, was daran so gut ist.
hat deine Denkweise verändert
Imperativer Stil – Dies ist der Ansatz, den die Java-Sprache seit ihrer Einführung bietet. Bei diesem Stil müssen wir Java bei jedem Schritt mitteilen, was es tun soll, und dann zusehen, wie es es tatsächlich Schritt für Schritt ausführt. Das ist natürlich gut, wirkt aber etwas rudimentär. Der Code sieht etwas ausführlich aus und wir wünschen uns, dass die Sprache etwas intelligenter wird. Wir sollten ihr einfach sagen, was wir wollen, anstatt ihr zu sagen, wie es geht. Glücklicherweise kann Java uns endlich dabei helfen, diesen Wunsch zu verwirklichen. Schauen wir uns einige Beispiele an, um die Vorteile und Unterschiede dieses Stils zu verstehen.
normaler Weg
Beginnen wir mit zwei bekannten Beispielen. Dies ist eine Befehlsmethode, um zu überprüfen, ob Chicago in der angegebenen Städtesammlung enthalten ist. Denken Sie daran, dass der in diesem Buch aufgeführte Code nur ein Teilfragment ist.
Kopieren Sie den Codecode wie folgt:
boolescher Wert gefunden = falsch;
for(String Stadt: Städte) {
if(city.equals("Chicago")) {
gefunden = wahr;
brechen;
}
}
System.out.println("Chicago gefunden?:" + gefunden);
Diese Imperativversion sieht etwas ausführlich und rudimentär aus; sie ist in mehrere Ausführungsteile unterteilt. Initialisieren Sie zunächst ein boolesches Tag mit dem Namen „found“ und durchlaufen Sie dann jedes Element in der Sammlung. Wenn die gesuchte Stadt gefunden wird, verlassen Sie die Schleife und geben Sie schließlich die Suchergebnisse aus.
ein besserer Weg
Nach dem Lesen dieses Codes werden aufmerksame Java-Programmierer schnell einen prägnanteren und klareren Weg finden, wie diesen:
Kopieren Sie den Codecode wie folgt:
System.out.println("Chicago gefunden?:" + towns.contains("Chicago"));
Dies ist auch ein zwingender Schreibstil – die Methode „contains“ übernimmt dies direkt für uns.
tatsächliche Verbesserungen
Das Schreiben von Code wie diesem hat mehrere Vorteile:
1. Kein Ärger mehr mit dieser veränderlichen Variablen
2. Kapseln Sie die Iteration in die unterste Ebene
3. Der Code ist einfacher
4. Der Code ist klarer und fokussierter
5. Machen Sie weniger Umwege und integrieren Sie Code und Geschäftsanforderungen enger
6. Weniger fehleranfällig
7. Leicht zu verstehen und zu warten
Nehmen wir ein komplizierteres Beispiel.
Dieses Beispiel ist zu einfach. Die zwingende Abfrage, ob ein Element in einer Sammlung vorhanden ist, ist überall in Java zu sehen. Nehmen wir nun an, dass wir die imperative Programmierung verwenden möchten, um einige fortgeschrittenere Vorgänge auszuführen, z. B. das Parsen von Dateien, die Interaktion mit Datenbanken, das Aufrufen von WEB-Diensten, die gleichzeitige Programmierung usw. Jetzt können wir Java verwenden, um prägnanteren, eleganteren und fehlerfreien Code zu schreiben, nicht nur in diesem einfachen Szenario.
der alte Weg
Schauen wir uns ein anderes Beispiel an. Wir legen eine Preisspanne fest und berechnen den rabattierten Gesamtpreis auf unterschiedliche Weise.
Kopieren Sie den Codecode wie folgt:
final List<BigDecimal>prices = Arrays.asList(
new BigDecimal("10"), new BigDecimal("30"), new BigDecimal("17"),
neues BigDecimal("20"), neues BigDecimal("15"), neues BigDecimal("18"),
new BigDecimal("45"), new BigDecimal("12"));
Gehen wir davon aus, dass es einen Rabatt von 10 % gibt, wenn der Betrag 20 Yuan übersteigt. Lassen Sie uns diesen zunächst auf die übliche Weise umsetzen.
Kopieren Sie den Codecode wie folgt:
BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;
for(BigDecimal Preis : Preise) {
if(price.compareTo(BigDecimal.valueOf(20)) > 0)
totalOfDiscountedPrices =
totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9)));
}
System.out.println("Summe der ermäßigten Preise: " + totalOfDiscountedPrices);
Dieser Code sollte sehr vertraut sein. Verwenden Sie zunächst eine Variable, um den Gesamtpreis zu speichern. Durchlaufen Sie dann alle Preise, berechnen Sie die ermäßigten Preise und drucken Sie sie schließlich zum Gesamtpreis aus Preis nach Rabatt.
Hier ist die Ausgabe des Programms:
Kopieren Sie den Codecode wie folgt:
Summe der ermäßigten Preise: 67,5
Das Ergebnis ist völlig korrekt, aber der Code ist etwas chaotisch. Es ist nicht unsere Schuld, dass wir nur so schreiben können, wie wir es haben. Allerdings ist ein solcher Code etwas rudimentär. Er leidet nicht nur unter grundlegender Paranoia, sondern verstößt auch gegen das Prinzip der Einzelverantwortung. Wenn Sie von zu Hause aus arbeiten und Kinder haben, die Programmierer werden möchten, müssen Sie Ihren Code verstecken, damit sie ihn nicht sehen und enttäuscht aufseufzen und sagen: „Verdienen Sie Ihren Lebensunterhalt damit?“
Es gibt einen besseren Weg
Wir können es besser machen – und zwar viel besser. Unser Code ist ein bisschen wie eine Anforderungsspezifikation. Dies kann die Lücke zwischen Geschäftsanforderungen und implementiertem Code verringern und die Möglichkeit einer Fehlinterpretation von Anforderungen verringern.
Wir lassen nicht mehr zu, dass Java eine Variable erstellt und sie endlos zuweist. Wir müssen mit ihr von einer höheren Abstraktionsebene aus kommunizieren, wie im folgenden Code.
Kopieren Sie den Codecode wie folgt:
final BigDecimal totalOfDiscountedPrices =
Preise.stream()
.filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
.map(Preis -> price.multiply(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("Summe der ermäßigten Preise: " + totalOfDiscountedPrices);
Lesen Sie es laut vor – filtern Sie Preise über 20 Yuan heraus, wandeln Sie sie in ermäßigte Preise um und addieren Sie sie dann. Dieser Code entspricht genau dem Prozess, den wir zur Beschreibung unserer Anforderungen verwendet haben. In Java ist es auch sehr praktisch, eine lange Codezeile zu falten und sie wie oben beschrieben zeilenweise entsprechend dem Punkt vor dem Methodennamen auszurichten.
Der Code ist sehr einfach, aber wir verwenden viele neue Dinge in Java8. Zuerst rufen wir eine Stream-Methode der Preisliste auf. Dies öffnet die Tür zu unzähligen praktischen Iteratoren, auf die wir später noch eingehen werden.
Wir verwenden einige spezielle Methoden wie Filter und Map, anstatt die gesamte Liste direkt zu durchlaufen. Diese Methoden ähneln denen im JDK, die wir zuvor verwendet haben. Sie akzeptieren eine anonyme Funktion – einen Lambda-Ausdruck – als Parameter. (Wir werden dies später ausführlich besprechen). Wir rufen die Methode Reduce() auf, um die Summe der von der Methode Map() zurückgegebenen Preise zu berechnen.
Genau wie bei der Methode „contains“ ist der Schleifenkörper ausgeblendet. Allerdings ist die Kartenmethode (und Filtermethode) viel komplizierter. Es ruft den übergebenen Lambda-Ausdruck auf, um jeden Preis in der Preisliste zu berechnen, und fügt das Ergebnis in eine neue Sammlung ein. Schließlich rufen wir die Methode „reduce“ für diese neue Sammlung auf, um das Endergebnis zu erhalten.
Dies ist die Ausgabe des obigen Codes:
Kopieren Sie den Codecode wie folgt:
Summe der ermäßigten Preise: 67,5
Bereiche mit Verbesserungspotenzial
Dies ist eine deutliche Verbesserung gegenüber der vorherigen Implementierung:
1. Gut strukturiert, aber nicht überladen
2. Keine Operationen auf niedriger Ebene
3. Einfache Erweiterung oder Änderung der Logik
4. Iteration durch Methodenbibliothek
5. Effizient; verzögerte Auswertung des Schleifenkörpers
6. Leicht parallelisierbar
Im Folgenden werden wir darüber sprechen, wie Java dies implementiert.
Lambda-Ausdrücke sind hier, um die Welt zu retten
Lambda-Ausdrücke sind eine Abkürzung, die uns die Probleme der imperativen Programmierung erspart. Diese neue Funktion von Java hat unsere ursprüngliche Programmiermethode verändert und den von uns geschriebenen Code nicht nur prägnanter und eleganter gemacht, er ist weniger fehleranfällig, sondern auch effizienter, einfacher zu optimieren, zu verbessern und zu parallelisieren.
Abschnitt 2: Der größte Gewinn durch funktionale Programmierung
Code im funktionalen Stil hat ein höheres Signal-Rausch-Verhältnis; es wird weniger Code geschrieben, aber pro Zeile oder Ausdruck wird mehr getan. Im Vergleich zur imperativen Programmierung hat uns die funktionale Programmierung viele Vorteile gebracht:
Es werden explizite Änderungen oder Zuweisungen von Variablen vermieden, die häufig Fehlerquellen darstellen und die Parallelisierung des Codes erschweren. Bei der Befehlszeilenprogrammierung weisen wir der Variable totalOfDiscountedPrices im Schleifenkörper kontinuierlich Werte zu. Im funktionalen Stil erfährt der Code keine expliziten Änderungsvorgänge mehr. Je weniger Variablen geändert werden, desto weniger Fehler weist der Code auf.
Funktionsstilcode kann leicht parallelisiert werden. Wenn die Berechnung zeitaufwändig ist, können wir die Listenelemente problemlos gleichzeitig ausführen. Wenn wir den Imperativcode parallelisieren möchten, müssen wir uns auch um die Probleme kümmern, die durch die gleichzeitige Änderung der Variable totalOfDiscountedPrices verursacht werden. In der funktionalen Programmierung greifen wir erst auf diese Variable zu, nachdem sie vollständig verarbeitet wurde, wodurch Bedenken hinsichtlich der Thread-Sicherheit beseitigt werden.
Code ist ausdrucksvoller. Die imperative Programmierung ist in mehrere Schritte unterteilt, um zu erklären, was zu tun ist – einen Initialisierungswert erstellen, Preise durchlaufen, Rabattpreise zu Variablen hinzufügen usw. –, während bei der funktionalen Programmierung nur die Kartenmethode der Liste einen Wert einschließlich des Rabatts zurückgeben muss . Erstellen Sie einfach eine neue Preisliste und sammeln Sie diese dann.
Die funktionale Programmierung ist einfacher; es ist weniger Code erforderlich, um das gleiche Ergebnis zu erzielen wie die imperative Programmierung. Saubererer Code bedeutet, dass weniger Code geschrieben, weniger gelesen und gewartet werden muss – siehe „Ist weniger prägnant genug, um prägnant zu sein?“ auf Seite 7.
Funktionscode ist intuitiver – das Lesen des Codes ist wie das Beschreiben des Problems – und ist leicht zu verstehen, sobald wir mit der Syntax vertraut sind. Die Map-Methode führt die angegebene Funktion (berechnet den Rabattpreis) für jedes Element der Sammlung aus und gibt dann die Ergebnismenge zurück, wie in der folgenden Abbildung dargestellt.
Abbildung 1 – Map führt die angegebene Funktion für jedes Element in der Sammlung aus
Mit Lambda-Ausdrücken können wir die Leistungsfähigkeit der funktionalen Programmierung in Java voll ausschöpfen. Mithilfe eines funktionalen Stils können Sie Code schreiben, der aussagekräftiger und prägnanter ist, weniger Zuweisungen aufweist und weniger Fehler aufweist.
Die Unterstützung der objektorientierten Programmierung ist ein großer Vorteil von Java. Funktionale Programmierung und objektorientierte Programmierung schließen sich nicht gegenseitig aus. Der eigentliche Stilwechsel erfolgt von der Befehlszeilenprogrammierung zur deklarativen Programmierung. In Java 8 können Funktionalität und Objektorientierung effektiv integriert werden. Wir können weiterhin den OOP-Stil verwenden, um Domänenentitäten und ihre Zustände und Beziehungen zu modellieren. Darüber hinaus können wir Funktionen auch zur Modellierung von Verhalten oder Zustandsübergängen, Arbeitsabläufen und Datenverarbeitung verwenden und zusammengesetzte Funktionen erstellen.
Abschnitt 3: Warum einen funktionalen Stil verwenden?
Wir haben die Vorteile der funktionalen Programmierung gesehen, aber lohnt es sich, diesen neuen Stil zu verwenden? Ist das nur eine kleine Verbesserung oder eine komplette Änderung? Es müssen noch viele praktische Fragen beantwortet werden, bevor wir uns wirklich damit befassen.
Kopieren Sie den Codecode wie folgt:
Xiao Ming fragte:
Bedeutet weniger Code Einfachheit?
Einfachheit bedeutet weniger, aber nicht Unordnung. Letztlich bedeutet es, die Absicht effektiv zum Ausdruck bringen zu können. Die Vorteile sind weitreichend.
Das Schreiben von Code ist wie das Anhäufen von Zutaten. Einfachheit bedeutet, Zutaten zu Gewürzen mischen zu können. Das Schreiben von prägnantem Code erfordert harte Arbeit. Es muss weniger Code gelesen werden und der wirklich nützliche Code ist für Sie transparent. Ein Shortcode, der schwer zu verstehen ist oder Details verbirgt, ist eher kurz als prägnant.
Einfacher Code bedeutet eigentlich agiles Design. Einfacher Code ohne Bürokratie. Das bedeutet, dass wir Ideen schnell ausprobieren, weitermachen können, wenn sie gut funktionieren, und schnell überspringen können, wenn sie nicht gut funktionieren.
Das Schreiben von Code in Java ist nicht schwierig und die Syntax ist einfach. Und wir kennen die vorhandenen Bibliotheken und APIs bereits sehr gut. Wirklich schwierig ist es, damit Anwendungen auf Unternehmensebene zu entwickeln und zu warten.
Wir müssen sicherstellen, dass Kollegen die Datenbankverbindung zum richtigen Zeitpunkt schließen, dass sie nicht weiterhin Transaktionen belegen, dass Ausnahmen auf der entsprechenden Ebene korrekt behandelt werden, dass Sperren korrekt erworben und freigegeben werden usw.
Für sich genommen ist jedes dieser Probleme keine große Sache. In Kombination mit der Komplexität des Fachgebiets wird das Problem jedoch sehr schwierig, die Entwicklungsressourcen sind knapp und die Wartung schwierig.
Was würde passieren, wenn wir diese Strategien in viele kleine Codeteile kapseln und sie die Einschränkungsverwaltung unabhängig durchführen lassen würden? Dann müssen wir nicht ständig Energie aufwenden, um Strategien umzusetzen. Das ist eine enorme Verbesserung. Schauen wir uns an, wie die funktionale Programmierung dies bewerkstelligt.
Verrückte Iterationen
Wir haben verschiedene Iterationen geschrieben, um Listen, Mengen und Karten zu verarbeiten. Die Verwendung von Iteratoren in Java ist weit verbreitet, aber zu kompliziert. Sie beanspruchen nicht nur mehrere Codezeilen, sondern sind auch schwer zu kapseln.
Wie durchlaufen wir die Sammlung und drucken sie aus? Sie können eine for-Schleife verwenden. Wie filtern wir einige Elemente aus der Sammlung heraus? Verwenden Sie weiterhin eine for-Schleife, aber Sie müssen einige zusätzliche veränderbare Variablen hinzufügen. Wie kann man diese Werte nach der Auswahl verwenden, um den Endwert zu ermitteln, z. B. den Minimalwert, den Maximalwert, den Durchschnittswert usw.? Dann müssen Sie die Variablen recyceln und ändern.
Diese Art der Iteration ist wie ein Allheilmittel, sie kann alles, aber alles ist spärlich. Java bietet jetzt integrierte Iteratoren für viele Operationen: zum Beispiel solche, die nur Schleifen ausführen, solche, die Kartenoperationen ausführen, solche, die Werte filtern, solche, die Operationen reduzieren, und es gibt viele praktische Funktionen wie Maximum, Minimum und Durchschnitt usw. Darüber hinaus lassen sich diese Vorgänge gut kombinieren, sodass wir sie zusammenfügen können, um eine Geschäftslogik zu implementieren, die einfach ist und weniger Code erfordert. Darüber hinaus ist der geschriebene Code gut lesbar, da er logisch mit der Reihenfolge der Problembeschreibung übereinstimmt. Wir werden mehrere solcher Beispiele in Kapitel 2, Verwenden von Sammlungen, auf Seite 19 sehen, und dieses Buch ist voll von solchen Beispielen.
Strategie anwenden
Richtlinien werden in unternehmensweiten Anwendungen implementiert. Beispielsweise müssen wir aus Sicherheitsgründen bestätigen, dass eine Operation korrekt authentifiziert wurde, wir müssen sicherstellen, dass die Transaktion schnell ausgeführt werden kann und das Änderungsprotokoll korrekt aktualisiert wird. Bei diesen Aufgaben handelt es sich normalerweise um einen gewöhnlichen Code auf der Serverseite, ähnlich dem folgenden Pseudocode:
Kopieren Sie den Codecode wie folgt:
Transaktiontransaktion = getFromTransactionFactory();
//... Operation, die innerhalb der Transaktion ausgeführt werden soll ...
checkProgressAndCommitOrRollbackTransaction();
UpdateAuditTrail();
Bei diesem Ansatz gibt es zwei Probleme. Erstens führt dies oft zu einer Verdoppelung des Aufwands und erhöht auch die Wartungskosten. Zweitens vergisst man leicht Ausnahmen, die im Geschäftscode ausgelöst werden können und sich auf den Transaktionslebenszyklus und die Aktualisierung des Änderungsprotokolls auswirken können. Dies sollte mithilfe von Try- und Final-Blöcken implementiert werden, aber jedes Mal, wenn jemand diesen Code berührt, müssen wir erneut bestätigen, dass diese Strategie nicht zerstört wurde.
Es gibt noch eine andere Möglichkeit: Wir können die Fabrik entfernen und diesen Code davor einfügen. Anstatt das Transaktionsobjekt abzurufen, übergeben Sie den ausgeführten Code an eine gut gepflegte Funktion, wie folgt:
Kopieren Sie den Codecode wie folgt:
runWithinTransaction((Transaktionstransaktion) -> {
//... Operation, die innerhalb der Transaktion ausgeführt werden soll ...
});
Es ist ein kleiner Schritt für Sie, aber er erspart Ihnen viel Ärger. Die Strategie, den Status zu überprüfen und gleichzeitig das Protokoll zu aktualisieren, wird abstrahiert und in die Methode runWithinTransaction gekapselt. Wir senden dieser Methode einen Code, der im Kontext einer Transaktion ausgeführt werden muss. Wir müssen uns keine Sorgen mehr machen, dass jemand vergisst, diesen Schritt auszuführen oder die Ausnahme nicht ordnungsgemäß behandelt. Die Funktion, die die Richtlinie implementiert, kümmert sich bereits darum.
In Kapitel 5 erfahren Sie, wie Sie Lambda-Ausdrücke verwenden, um diese Strategie anzuwenden.
Expansionsstrategie
Strategien scheinen überall zu sein. Unternehmensanwendungen müssen sie nicht nur anwenden, sondern auch erweitern. Wir hoffen, einige Vorgänge über einige Konfigurationsinformationen hinzufügen oder löschen zu können. Mit anderen Worten, wir können sie verarbeiten, bevor die Kernlogik des Moduls ausgeführt wird. Dies kommt in Java sehr häufig vor, muss aber im Voraus durchdacht und entworfen werden.
Komponenten, die erweitert werden müssen, verfügen in der Regel über eine oder mehrere Schnittstellen. Wir müssen die Schnittstelle und die hierarchische Struktur der Implementierungsklassen sorgfältig entwerfen. Das mag zwar gut funktionieren, hinterlässt aber eine Menge Schnittstellen und Klassen, die gepflegt werden müssen. Ein solches Design kann leicht unhandlich und schwer zu warten werden und letztendlich den Zweck der Skalierung von vornherein zunichtemachen.
Es gibt eine andere Lösung – funktionale Schnittstellen und Lambda-Ausdrücke, mit denen wir skalierbare Strategien entwerfen können. Wir müssen keine neue Schnittstelle erstellen oder dem gleichen Methodennamen folgen. Wir können uns mehr auf die zu implementierende Geschäftslogik konzentrieren, die wir bei der Verwendung von Lambda-Ausdrücken zur Dekoration auf Seite 73 erwähnen werden.
Parallelität leicht gemacht
Eine große Anwendung nähert sich einem Release-Meilenstein, als plötzlich ein ernstes Leistungsproblem auftritt. Das Team stellte schnell fest, dass der Leistungsengpass in einem riesigen Modul lag, das riesige Datenmengen verarbeitet. Jemand im Team schlug vor, dass die Systemleistung verbessert werden könnte, wenn die Vorteile von Multicore voll ausgenutzt werden könnten. Wenn dieses riesige Modul jedoch im alten Java-Stil geschrieben wird, wird die Freude an diesem Vorschlag bald zunichte gemacht.
Das Team erkannte schnell, dass die Umstellung dieses Ungetüms von der seriellen Ausführung auf die parallele Ausführung viel Aufwand erfordern würde, zusätzliche Komplexität mit sich bringen und leicht Fehler im Zusammenhang mit Multithreading verursachen würde. Gibt es nicht einen besseren Weg, die Leistung zu verbessern?
Ist es möglich, dass serieller und paralleler Code gleich sind, unabhängig davon, ob Sie die serielle oder parallele Ausführung wählen, genau wie das Drücken eines Schalters und das Ausdrücken Ihrer Idee?
Es hört sich so an, als wäre dies nur in Narnia möglich, aber wenn wir uns funktional vollständig weiterentwickeln, wird all dies Realität. Integrierte Iteratoren und ein funktionaler Stil beseitigen die letzte Hürde für die Parallelisierung. Das Design des JDK ermöglicht den Wechsel zwischen serieller und paralleler Ausführung mit nur wenigen unauffälligen Codeänderungen, die wir unter „Den Sprung zur Parallelisierung vollenden“ auf Seite 145 erwähnen werden.
Geschichten erzählen
Bei der Umsetzung von Geschäftsanforderungen in Code-Implementierung gehen viele Dinge verloren. Je mehr verloren geht, desto höher ist die Fehlerwahrscheinlichkeit und die Verwaltungskosten. Wenn der Code so aussieht, als würde er die Anforderungen beschreiben, ist er leichter zu lesen, lässt sich leichter mit den Anforderungsleuten besprechen und es ist einfacher, ihre Bedürfnisse zu erfüllen.
Sie hören beispielsweise, wie der Produktmanager sagt: „Ermitteln Sie die Kurse aller Aktien, finden Sie diejenigen mit Preisen über 500 Yuan und berechnen Sie das Gesamtvermögen, das Dividenden zahlen kann.“ Mit den neuen Funktionen von Java können Sie schreiben:
Kopieren Sie den Codecode wie folgt:
tickers.map(StockUtil::getprice).filter(StockUtil::priceIsLessThan500).sum()
Dieser Konvertierungsprozess ist nahezu verlustfrei, da im Grunde nichts konvertiert werden muss. Das ist funktionaler Stil in Aktion, und Sie werden im gesamten Buch noch viele weitere Beispiele dafür finden, insbesondere in Kapitel 8, Programme mit Lambda-Ausdrücken erstellen, Seite 137.
Konzentrieren Sie sich auf Quarantäne
Bei der Systementwicklung müssen in der Regel das Kerngeschäft und die dafür erforderliche feinkörnige Logik isoliert werden. Beispielsweise möchte ein Auftragsverarbeitungssystem möglicherweise unterschiedliche Besteuerungsstrategien für unterschiedliche Transaktionsquellen verwenden. Durch die Isolierung der Steuerberechnungen vom Rest der Verarbeitungslogik wird der Code wiederverwendbar und skalierbarer.
In der objektorientierten Programmierung nennen wir dies Bedenkenisolation, und zur Lösung dieses Problems wird normalerweise das Strategiemuster verwendet. Die Lösung besteht im Allgemeinen darin, einige Schnittstellen und Implementierungsklassen zu erstellen.
Wir können den gleichen Effekt mit weniger Code erzielen. Auch eigene Produktideen können wir schnell ausprobieren, ohne dass wir uns jede Menge Code ausdenken und stagnieren müssen. Wir werden im Abschnitt „Bedenkenisolierung mit Lambda-Ausdrücken“ auf Seite 63 näher erläutern, wie Sie dieses Muster erstellen und die Besorgnisisolierung mithilfe einfacher Funktionen durchführen.
faule Bewertung
Bei der Entwicklung von Anwendungen auf Unternehmensebene können wir mit WEB-Diensten interagieren, Datenbanken aufrufen, XML verarbeiten usw. Es gibt viele Operationen, die wir durchführen müssen, aber nicht alle davon sind ständig erforderlich. Das Vermeiden bestimmter Vorgänge oder zumindest das Verzögern einiger vorübergehend unnötiger Vorgänge ist eine der einfachsten Möglichkeiten, die Leistung zu verbessern oder die Start- und Reaktionszeit des Programms zu verkürzen.
Dies ist nur eine Kleinigkeit, aber es erfordert viel Arbeit, sie auf reine OOP-Art umzusetzen. Um die Initialisierung einiger schwerer Objekte zu verzögern, müssen wir verschiedene Objektreferenzen verarbeiten, auf Nullzeiger prüfen usw.
Wenn Sie jedoch die neue optionale Klasse und einige von ihr bereitgestellte funktionale Stil-APIs verwenden, wird dieser Prozess sehr einfach und der Code wird klarer. Wir werden dies in der verzögerten Initialisierung auf Seite 105 besprechen.
Testbarkeit verbessern
Je weniger Verarbeitungslogik der Code hat, desto geringer ist die Wahrscheinlichkeit, dass Fehler korrigiert werden. Im Allgemeinen ist Funktionscode einfacher zu ändern und zu testen.
Darüber hinaus können Lambda-Ausdrücke genau wie Kapitel 4, „Entwerfen mit Lambda-Ausdrücken“ und Kapitel 5, „Verwenden von Ressourcen“, als leichte Scheinobjekte verwendet werden, um Ausnahmetests klarer und verständlicher zu machen. Lambda-Ausdrücke können auch als hervorragende Testhilfe dienen. Viele gängige Testfälle können Lambda-Ausdrücke akzeptieren und verarbeiten. Auf diese Weise geschriebene Testfälle können das Wesentliche der Funktionalität erfassen, die einem Regressionstest unterzogen werden muss. Gleichzeitig können verschiedene zu testende Implementierungen durch die Übergabe unterschiedlicher Lambda-Ausdrücke abgeschlossen werden.
Auch JDK-eigene automatisierte Testfälle sind ein gutes Anwendungsbeispiel für Lambda-Ausdrücke – wer mehr wissen möchte, kann sich den Quellcode im OpenJDK-Repository ansehen. Anhand dieser Testprogramme können Sie sehen, wie Lambda-Ausdrücke die Schlüsselverhaltensweisen des Testfalls parametrisieren. Sie erstellen das Testprogramm beispielsweise wie folgt: „Erstellen Sie einen Container für die Ergebnisse“ und fügen Sie dann „Einige parametrisierte Nachbedingungen hinzufügen“ hinzu.
Wir haben gesehen, dass funktionale Programmierung uns nicht nur das Schreiben von qualitativ hochwertigem Code ermöglicht, sondern auch verschiedene Probleme während des Entwicklungsprozesses elegant löst. Dies bedeutet, dass die Entwicklung von Programmen schneller und einfacher wird und weniger Fehler verursachen – sofern Sie einige Richtlinien befolgen, die wir später vorstellen.
Abschnitt 4: Evolution statt Revolution
Wir müssen nicht auf eine andere Sprache umsteigen, um die Vorteile der funktionalen Programmierung nutzen zu können; wir müssen lediglich die Art und Weise ändern, wie wir Java verwenden. Sprachen wie C++, Java und C# unterstützen alle imperative und objektorientierte Programmierung. Aber jetzt beginnen sie, sich der funktionalen Programmierung zuzuwenden. Wir haben uns gerade beide Codestile angesehen und die Vorteile besprochen, die funktionale Programmierung mit sich bringen kann. Schauen wir uns nun einige seiner Schlüsselkonzepte und Beispiele an, die uns beim Erlernen dieses neuen Stils helfen sollen.
Das Java-Sprachentwicklungsteam hat viel Zeit und Energie darauf verwendet, der Java-Sprache und dem JDK funktionale Programmierfunktionen hinzuzufügen. Um die damit verbundenen Vorteile nutzen zu können, müssen wir zunächst einige neue Konzepte einführen. Wir können die Qualität unseres Codes verbessern, solange wir die folgenden Regeln befolgen:
1. Deklarativ
2. Unveränderlichkeit fördern
3. Vermeiden Sie Nebenwirkungen
4. Bevorzugen Sie Ausdrücke gegenüber Anweisungen
5. Entwerfen Sie mit Funktionen höherer Ordnung
Werfen wir einen Blick auf diese praktischen Richtlinien.
deklarativ
Der Kern dessen, was wir als imperative Programmierung kennen, ist Variabilität und befehlsgesteuerte Programmierung. Wir erstellen Variablen und ändern dann kontinuierlich ihre Werte. Wir stellen auch detaillierte Anweisungen zur Verfügung, die ausgeführt werden müssen, z. B. das Generieren des Index-Flags der Iteration, das Erhöhen seines Werts, das Überprüfen, ob die Schleife beendet wurde, das Aktualisieren des N-ten Elements des Arrays usw. In der Vergangenheit konnten wir Code aufgrund der Eigenschaften von Tools und Hardwareeinschränkungen nur auf diese Weise schreiben. Wir haben auch gesehen, dass bei einer unveränderlichen Sammlung die deklarative Methode „Contains“ einfacher zu verwenden ist als die Methode „Imperativ“. Alle schwierigen Probleme und Operationen auf niedriger Ebene werden in Bibliotheksfunktionen implementiert, und wir müssen uns um diese Details nicht mehr kümmern. Der Einfachheit halber sollten wir auch die deklarative Programmierung verwenden. Unveränderlichkeit und deklarative Programmierung sind die Essenz der funktionalen Programmierung, und jetzt macht Java sie endlich Wirklichkeit.
Unveränderlichkeit fördern
Code mit veränderlichen Variablen verfügt über viele Aktivitätspfade. Je mehr Dinge Sie ändern, desto einfacher ist es, die ursprüngliche Struktur zu zerstören und mehr Fehler einzuführen. Code mit mehreren zu ändernden Variablen ist schwer zu verstehen und schwer zu parallelisieren. Unveränderlichkeit beseitigt diese Sorgen im Wesentlichen. Java unterstützt Unveränderlichkeit, erfordert sie aber nicht – aber wir können es. Wir müssen die alte Gewohnheit ändern, den Objektstatus zu ändern. Wir sollten so oft wie möglich unveränderliche Objekte verwenden. Wenn Sie Variablen, Mitglieder und Parameter deklarieren, versuchen Sie, sie als endgültig zu deklarieren, genau wie Joshua Blochs berühmtes Sprichwort in „Effective Java“: „Behandeln Sie Objekte als unveränderlich.“ Versuchen Sie beim Erstellen von Objekten, unveränderliche Objekte wie String zu erstellen. Versuchen Sie beim Erstellen einer Sammlung, eine unveränderliche oder nicht veränderbare Sammlung zu erstellen, indem Sie beispielsweise Methoden wie Arrays.asList() und Collections' unmodifiableList() verwenden. Indem wir Variabilität vermeiden, können wir reine Funktionen schreiben, also Funktionen ohne Nebenwirkungen.
Nebenwirkungen vermeiden
Angenommen, Sie schreiben einen Code, um den Preis einer Aktie aus dem Internet abzurufen und ihn in eine gemeinsam genutzte Variable zu schreiben. Wenn wir viele Preise abrufen müssen, müssen wir diese zeitaufwändigen Vorgänge nacheinander durchführen. Wenn wir die Leistungsfähigkeit von Multithreading nutzen wollen, müssen wir uns mit den Problemen des Threadings und der Synchronisierung auseinandersetzen, um Race Conditions zu verhindern. Das Endergebnis ist, dass die Leistung des Programms sehr schlecht ist und die Leute vergessen, zu essen und zu schlafen, um den Thread aufrechtzuerhalten. Wenn Nebenwirkungen eliminiert würden, könnten wir diese Probleme vollständig vermeiden. Eine Funktion ohne Nebenwirkungen fördert die Unveränderlichkeit und verändert keine Eingaben oder irgendetwas anderes in ihrem Gültigkeitsbereich. Diese Art von Funktion ist gut lesbar, weist wenige Fehler auf und ist leicht zu optimieren. Da es keine Nebenwirkungen gibt, besteht kein Grund zur Sorge über Rennbedingungen oder gleichzeitige Änderungen. Darüber hinaus können wir diese Funktionen problemlos parallel ausführen, was wir auf Seite 145 besprechen werden.
Bevorzugen Sie Ausdrücke
Aussagen sind ein heißes Eisen, weil sie Änderungen erzwingen. Ausdrücke fördern Unveränderlichkeit und Funktionszusammensetzung. Beispielsweise verwenden wir zunächst die for-Anweisung, um den Gesamtpreis nach Rabatten zu berechnen. Ein solcher Code führt zu Variabilität und ausführlichem Code. Die Verwendung ausdrucksstärkerer, deklarativer Versionen der Karten- und Summenmethoden vermeidet nicht nur Änderungsoperationen, sondern ermöglicht auch die Verkettung von Funktionen. Beim Schreiben von Code sollten Sie versuchen, Ausdrücke anstelle von Anweisungen zu verwenden. Dadurch wird der Code einfacher und verständlicher. Der Code wird entlang der Geschäftslogik ausgeführt, genau wie bei der Beschreibung des Problems. Eine prägnante Version lässt sich zweifellos einfacher anpassen, wenn sich die Anforderungen ändern.
Entwerfen Sie mit Funktionen höherer Ordnung
Java erzwingt keine Unveränderlichkeit wie funktionale Sprachen wie Haskell, aber es ermöglicht uns, Variablen zu ändern. Deshalb ist Java keine rein funktionale Programmiersprache und wird es auch nie sein. Wir können jedoch Funktionen höherer Ordnung für die funktionale Programmierung in Java verwenden. Funktionen höherer Ordnung bringen die Wiederverwendung auf die nächste Ebene. Mit Funktionen höherer Ordnung können wir ausgereiften Code, der klein, spezialisiert und hochkohäsiv ist, problemlos wiederverwenden. In OOP sind wir es gewohnt, Objekte an Methoden zu übergeben, neue Objekte in den Methoden zu erstellen und die Objekte dann zurückzugeben. Funktionen höherer Ordnung führen bei Funktionen dasselbe aus wie Methoden bei Objekten. Mit Funktionen höherer Ordnung können wir.
1. Funktion an Funktion übergeben
2. Erstellen Sie eine neue Funktion innerhalb der Funktion
3. Rückgabefunktionen innerhalb von Funktionen
Wir haben bereits ein Beispiel für die Übergabe von Parametern von einer Funktion an eine andere Funktion gesehen und später werden wir Beispiele für das Erstellen und Zurückgeben von Funktionen sehen. Schauen wir uns noch einmal das Beispiel der „Übergabe von Parametern an eine Funktion“ an:
Kopieren Sie den Codecode wie folgt:
Preise.stream()
.filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0) .map(price -> price.multiply(BigDecimal.valueOf(0.9)))
Erratum melden • diskutieren
.reduce(BigDecimal.ZERO, BigDecimal::add);
In diesem Code übergeben wir die Funktion „price -> price.multiply(BigDecimal.valueOf(0.9))“ an die Kartenfunktion. Die übergebene Funktion wird erstellt, wenn die Funktionszuordnung höherer Ordnung aufgerufen wird. Im Allgemeinen besteht eine Funktion aus einem Funktionskörper, einem Funktionsnamen, einer Parameterliste und einem Rückgabewert. Diese spontan erstellte Funktion verfügt über eine Parameterliste, gefolgt von einem Pfeil (->) und einem kurzen Funktionskörper. Die Parametertypen werden vom Java-Compiler abgeleitet und der Rückgabetyp ist ebenfalls implizit. Dies ist eine anonyme Funktion, sie hat keinen Namen. Aber wir nennen es keine anonyme Funktion, sondern einen Lambda-Ausdruck. Die Übergabe anonymer Funktionen als Parameter ist in Java nichts Neues; wir haben bereits oft anonyme innere Klassen übergeben. Selbst wenn eine anonyme Klasse nur eine Methode hat, müssen wir dennoch das Ritual durchlaufen, eine Klasse zu erstellen und zu instanziieren. Mit Lambda-Ausdrücken können wir eine schlanke Syntax genießen. Darüber hinaus waren wir es immer gewohnt, einige Konzepte in verschiedene Objekte zu abstrahieren, jetzt können wir einige Verhaltensweisen in Lambda-Ausdrücke abstrahieren. Das Programmieren mit diesem Codierungsstil erfordert noch einiges Nachdenken. Wir müssen unser bereits tief verwurzeltes zwingendes Denken in funktionales Denken umwandeln. Am Anfang mag es etwas schmerzhaft sein, aber Sie werden sich schnell daran gewöhnen. Wenn Sie sich weiter vertiefen, werden diese nicht funktionierenden APIs nach und nach zurückbleiben. Lassen Sie uns dieses Thema zunächst beenden. Werfen wir einen Blick darauf, wie Java mit Lambda-Ausdrücken umgeht. Früher haben wir immer Objekte an Methoden übergeben, jetzt können wir Funktionen speichern und weitergeben. Werfen wir einen Blick auf das Geheimnis hinter der Fähigkeit von Java, Funktionen als Parameter zu verwenden.
Abschnitt 5: Etwas Syntaxzucker hinzugefügt
Dies kann auch mit den Originalfunktionen von Java erreicht werden, aber Lambda-Ausdrücke fügen etwas syntaktischen Zucker hinzu, was einige Schritte einspart und unsere Arbeit einfacher macht. Der so geschriebene Code entwickelt sich nicht nur schneller, sondern bringt unsere Ideen auch besser zum Ausdruck. Viele Schnittstellen, die wir in der Vergangenheit verwendet haben, hatten nur eine Methode: Runnable, Callable usw. Diese Schnittstellen sind überall in der JDK-Bibliothek zu finden und dort, wo sie verwendet werden, können sie normalerweise mit einer Funktion ausgeführt werden. Bibliotheksfunktionen, die zuvor nur eine Ein-Methoden-Schnittstelle erforderten, können dank des syntaktischen Zuckers, der von funktionalen Schnittstellen bereitgestellt wird, jetzt leichtgewichtige Funktionen übergeben. Eine funktionale Schnittstelle ist eine Schnittstelle mit nur einer abstrakten Methode. Schauen Sie sich diese Schnittstellen mit nur einer Methode an: Runnable, Callable usw., diese Definition gilt für sie. Es gibt weitere solcher Schnittstellen in JDK8 – Funktion, Prädikat, Verbraucher, Lieferant usw. (Seite 157, Anhang 1 enthält eine detailliertere Schnittstellenliste). Funktionale Schnittstellen können über mehrere statische Methoden und Standardmethoden verfügen, die in der Schnittstelle implementiert sind. Wir können die Annotation @FunctionalInterface verwenden, um eine funktionale Schnittstelle zu kommentieren. Der Compiler verwendet diese Annotation nicht, kann aber den Typ dieser Schnittstelle klarer identifizieren. Nicht nur das, wenn wir eine Schnittstelle mit dieser Annotation kommentieren, überprüft der Compiler gewaltsam, ob er den Regeln der funktionalen Schnittstellen entspricht. Wenn eine Methode eine funktionale Schnittstelle als Parameter empfängt, umfassen die Parameter, die wir übergeben können,:
1. Anonyme innere Klassen, der älteste Weg
2. Lambda -Ausdruck, genau wie wir es in der Kartenmethode getan haben
3.. Verweis auf eine Methode oder einen Konstruktor (wir werden später darüber sprechen)
Wenn der Methodeparameter eine funktionale Schnittstelle ist, akzeptiert der Compiler gerne einen Lambda -Ausdruck oder eine Methodenreferenz als Parameter. Wenn wir einen Lambda -Ausdruck an eine Methode übergeben, wandelt der Compiler zunächst den Ausdruck in eine Instanz der entsprechenden funktionalen Grenzfläche um. Diese Transformation ist mehr als nur eine innere Klasse. Die Methoden dieser synchron erzeugten Instanz entsprechen den abstrakten Methoden der funktionalen Schnittstelle des Parameters. Beispielsweise empfängt die MAP -Methode die Funktion der Funktionsschnittstelle als Parameter. Beim Aufrufen der Kartenmethode erzeugt der Java -Compiler sie synchron, wie in der folgenden Abbildung gezeigt.
Die Parameter des Lambda -Expression müssen den Parametern der abstrakten Methode der Schnittstelle übereinstimmen. Diese erzeugte Methode gibt das Ergebnis des Lambda -Ausdrucks zurück. Wenn der Rückgabetyp nicht direkt mit der abstrakten Methode übereinstimmt, wandelt diese Methode den Rückgabewert in den entsprechenden Typ um. Wir hatten bereits einen Überblick darüber, wie Lambda -Ausdrücke an Methoden übergeben werden. Lassen Sie uns schnell überprüfen, worüber wir gerade gesprochen haben, und dann mit der Erforschung von Lambda -Ausdrücken beginnen.
Zusammenfassen
Dies ist ein völlig neuer Bereich von Java. Durch Funktionen höherer Ordnung können wir jetzt einen eleganten und fließenden Funktionscode für funktionale Stil schreiben. Der auf diese Weise geschriebene Code ist prägnant und leicht zu verstehen, hat nur wenige Fehler und fördert die Wartung und Parallelisierung. Der Java -Compiler arbeitet seine Magie, und wo wir funktionale Schnittstellenparameter erhalten, können wir Lambda -Ausdrücke oder Methodenreferenzen übergeben. Wir können jetzt in die Welt der Lambda -Ausdrücke eintreten und die JDK -Bibliotheken, die ihnen angepasst sind, um den Spaß an ihnen zu spüren. Im nächsten Kapitel beginnen wir mit den häufigsten festgelegten Operationen in der Programmierung und entfesseln die Leistung von Lambda -Ausdrücken.