Viele haben von funktionalen Sprachen wie Haskell und Clojure gehört. Aber es gibt zum Beispiel Sprachen wie Scala. Es kombiniert sowohl OOP als auch einen funktionalen Ansatz. Was ist mit dem guten alten Java? Ist es möglich, Programme in einem funktionalen Stil darauf zu schreiben und wie sehr kann es weh tun? Ja, es gibt Java 8 und Lambdas mit Streams. Dies ist ein großer Schritt für die Sprache, aber es reicht immer noch nicht aus. Ist es in dieser Situation möglich, sich etwas auszudenken? Es stellt sich ja heraus.
Lassen Sie uns zunächst herausfinden, was das Schreiben von Code in einem funktionalen Stil bedeutet. Erstens müssen wir nicht mit Variablen und Manipulationen mit ihnen arbeiten, sondern mit Ketten einiger Berechnungen. Im Wesentlichen eine Folge von Funktionen. Außerdem müssen wir spezielle Datenstrukturen haben. Beispielsweise sind Standard-Java-Sammlungen nicht geeignet. Es wird bald klar, warum.
Betrachten wir funktionale Strukturen genauer. Eine solche Struktur muss mindestens zwei Bedingungen erfüllen:
- unveränderlich - die Struktur muss unveränderlich sein. Dies bedeutet, dass wir den Zustand des Objekts im Stadium der Schöpfung festlegen und es als solches bis zum Ende seiner Existenz belassen. Ein klares Beispiel für eine Bedingungsverletzung: Standard ArrayList.
- persistent - Die Struktur sollte so lange wie möglich im Speicher gespeichert werden. Wenn wir ein Objekt erstellt haben, sollten wir anstelle eines neuen Objekts mit demselben Status das fertige verwenden. Formal behalten solche Strukturen bei Modifikation alle ihre vorherigen Zustände bei. Verweise auf diese Bedingungen müssen voll funktionsfähig bleiben.
Offensichtlich brauchen wir eine Lösung von Drittanbietern. Und es gibt eine solche Lösung: die
Vavr- Bibliothek. Heute ist es die beliebteste
Java- Bibliothek für die Arbeit in einem funktionalen Stil. Als nächstes werde ich die Hauptfunktionen der Bibliothek beschreiben. Viele, aber keineswegs alle Beispiele und Beschreibungen wurden der offiziellen Dokumentation entnommen.
Die wichtigsten Datenstrukturen der vavr- Bibliothek
Tupel
Eine der grundlegendsten und einfachsten funktionalen Datenstrukturen sind Tupel. Ein Tupel ist ein geordneter Satz fester Länge. Im Gegensatz zu Listen kann ein Tupel Daten eines beliebigen Typs enthalten.
Tuple tuple = Tuple.of(1, "blablabla", .0, 42L);
Das Abrufen des gewünschten Elements erfolgt durch Aufrufen des Felds mit der Artikelnummer im Tupel.
((Tuple4) tuple)._1
Bitte beachten Sie: Die Tupelindizierung beginnt bei 1! Um das gewünschte Element zu erhalten, müssen wir unser Objekt mit den entsprechenden Methoden in den gewünschten Typ konvertieren. Im obigen Beispiel haben wir ein Tupel mit 4 Elementen verwendet, was bedeutet, dass die Konvertierung vom Typ
Tuple4 sein muss . Tatsächlich hindert uns niemand daran, zunächst den richtigen Typ herzustellen.
Tuple4 tuple = Tuple.of(1, "blablabla", .0, 42L);
Top 3 vavr Sammlungen
Liste
Das Erstellen einer Liste mit vavr ist sehr einfach. Noch einfacher als ohne
vavr .
List.of(1, 2, 3)
Was können wir mit einer solchen Liste machen? Nun, erstens können wir daraus eine Standard-
Java- Liste machen.
final boolean containThree = List.of(1, 2, 3) .asJava() .stream() .anyMatch(x -> x == 3);
Tatsächlich ist dies jedoch nicht sehr notwendig, da Wir können zum Beispiel so vorgehen:
final boolean containThree = List.of(1, 2, 3) .find(x -> x == 1) .isDefined();
Im Allgemeinen enthält die Standardliste der
vavr- Bibliothek viele nützliche Methoden. Zum Beispiel gibt es eine ziemlich leistungsfähige Faltungsfunktion, mit der Sie eine Liste von Werten nach einer Regel und einem neutralen Element kombinieren können.
Ein wichtiger Punkt sollte hier beachtet werden. Wir haben funktionale Datenstrukturen, was bedeutet, dass wir ihren Zustand nicht ändern können. Wie wird unsere Liste umgesetzt? Arrays passen einfach nicht zu uns.
Verknüpfte Liste als StandardlisteLassen Sie uns eine einfach verknüpfte Liste mit unveränderlichen Objekten erstellen. Es wird ungefähr so aussehen:

Codebeispiel List list = List.of(1, 2, 3);
Jedes Element der Liste hat zwei Hauptmethoden: Erhalten des Kopfelements (Kopf) und aller anderen (Schwanz).
Wenn wir nun das erste Element in der Liste ändern möchten (von 1 auf 0), müssen wir eine neue Liste mit der Wiederverwendung der fertigen Teile erstellen.

Codebeispiel final List tailList = list.tail();
Und alle! Da unsere Objekte im Arbeitsblatt unveränderlich sind, erhalten wir eine thread-sichere und wiederverwendbare Sammlung. Elemente unserer Liste können überall in der Anwendung angewendet werden und sind absolut sicher!
Warteschlange
Eine weitere äußerst nützliche Datenstruktur ist die Warteschlange. Wie erstelle ich eine Warteschlange, um effektive und zuverlässige Programme in einem funktionalen Stil zu erstellen? Zum Beispiel können wir Datenstrukturen nehmen, die uns bereits bekannt sind: zwei Listen und ein Tupel.

Codebeispiel Queue<Integer> queue = Queue.of(1, 2, 3) .enqueue(4) .enqueue(5);
Wenn der erste endet, erweitern wir den zweiten und verwenden ihn zum Lesen.


Es ist wichtig zu beachten, dass die Warteschlange wie alle anderen Strukturen unverändert bleiben muss. Aber was nützt eine Warteschlange, die sich nicht ändert? In der Tat gibt es einen Trick. Als akzeptierten Wert der Warteschlange erhalten wir ein Tupel aus zwei Elementen. Erstens: das gewünschte Warteschlangenelement, zweitens: Was ist mit der Warteschlange ohne dieses Element passiert?
System.out.println(queue);
Streams
Die nächste wichtige Datenstruktur ist der Stream. Ein Stream ist ein Stream zur Ausführung einiger Aktionen für einen bestimmten, oft abstrakten Wertesatz.
Jemand könnte sagen, dass
Java 8 bereits über vollwertige Streams verfügt und wir überhaupt keine neuen benötigen. Ist es so?
Stellen Sie zunächst sicher, dass der
Java-Stream keine funktionierende Datenstruktur ist. Überprüfen Sie die Struktur auf Veränderlichkeit. Erstellen Sie dazu einen so kleinen Stream:
IntStream standardStream = IntStream.range(1, 10);
Wir werden alle Elemente im Stream sortieren:
standardStream.forEach(System.out::print);
Als Antwort erhalten wir die Ausgabe an die Konsole:
123456789 . Wiederholen wir die Brute-Force-Operation:
standardStream.forEach(System.out::print);
Hoppla, der folgende Fehler ist aufgetreten:
java.lang.IllegalStateException: stream has already been operated upon or closed
Tatsache ist, dass Standard-Streams nur eine Art Abstraktion über einen Iterator sind. Obwohl die Streams äußerlich äußerst unabhängig und mächtig erscheinen, sind die Minuspunkte der Iteratoren nicht verschwunden.
Zum Beispiel sagt die Definition eines Streams nichts über die Begrenzung der Anzahl von Elementen aus. Leider ist es im Iterator vorhanden, was bedeutet, dass es sich um Standard-Streams handelt.
Glücklicherweise löst die vavr-Bibliothek diese Probleme. Stellen Sie Folgendes sicher:
Stream stream = Stream.range(1, 10); stream.forEach(System.out::print); stream.forEach(System.out::print);
Als Antwort erhalten wir
123456789123456789 . Dies bedeutet, dass die erste Operation unseren Stream nicht „verdorben“ hat.
Versuchen wir, einen endlosen Stream zu erstellen:
Stream infiniteStream = Stream.from (1);
System.out.println (infiniteStream); // Stream (1,?)
Bitte beachten Sie: Beim Drucken eines Objekts erhalten wir keine unendliche Struktur, sondern das erste Element und ein Fragezeichen. Tatsache ist, dass jedes nachfolgende Element im Stream im laufenden Betrieb generiert wird. Dieser Ansatz wird als verzögerte Initialisierung bezeichnet. Er ist es, der es Ihnen ermöglicht, sicher mit solchen Strukturen zu arbeiten.
Wenn Sie noch nie mit unendlichen Datenstrukturen gearbeitet haben, denken Sie höchstwahrscheinlich: Warum ist das überhaupt notwendig? Aber sie können sehr bequem sein. Wir schreiben einen Stream, der eine beliebige Anzahl ungerader Zahlen zurückgibt, diese in eine Zeichenfolge konvertiert und ein Leerzeichen hinzufügt:
Stream oddNumbers = Stream .from(1, 2)
So einfach.
Allgemeine Struktur der Sammlungen
Nachdem wir die grundlegenden Strukturen besprochen haben, ist es Zeit, die allgemeine Architektur der
vavr- Funktionssammlungen zu betrachten:

Jedes Element der Struktur kann als iterierbar verwendet werden:
StringBuilder builder = new StringBuilder(); for (String word : List.of("one", "two", "tree")) { if (builder.length() > 0) { builder.append(", "); } builder.append(word); } System.out.println(builder.toString());
Aber Sie sollten zweimal überlegen und das Dock sehen, bevor Sie es verwenden. Mit der Bibliothek können Sie vertraute Dinge einfacher machen.
System.out.println(List.of("one", "two", "tree").mkString(", "));
Mit Funktionen arbeiten
Die Bibliothek verfügt über eine Reihe von Funktionen (8 Teile) und nützliche Methoden für die Arbeit mit ihnen. Sie sind gewöhnliche funktionale Schnittstellen mit vielen interessanten Methoden. Der Name der Funktionen hängt von der Anzahl der akzeptierten Argumente ab (von 0 bis 8). Zum Beispiel
akzeptiert Function0 keine Argumente,
Function1 ein Argument,
Function2 zwei usw.
Function2<String, String, String> combineName = (lastName, firstName) -> firstName + " " + lastName; System.out.println(combineName.apply("Griffin", "Peter"));
In den Funktionen der vavr-Bibliothek können wir viele coole Dinge tun. In Bezug auf die Funktionalität gehen sie der Standardfunktion, der BiFunktion usw. weit voraus. Zum Beispiel Curry. Currying ist die Konstruktion von Funktionen in Teilen. Schauen wir uns ein Beispiel an:
Wie Sie sehen können, ganz prägnant. Die
Curry- Methode ist extrem einfach, kann aber sehr nützlich sein.
Implementierung der Curry-Methode @Override default Function1<T1, Function1<T2, R>> curried() { return t1 -> t2 -> apply(t1, t2); }
Es gibt viele weitere nützliche Methoden im
Funktionssatz . Sie können beispielsweise das Rückgabeergebnis einer Funktion zwischenspeichern:
Function0<Double> hashCache = Function0.of(Math::random).memoized(); double randomValue1 = hashCache.apply(); double randomValue2 = hashCache.apply(); System.out.println(randomValue1 == randomValue2);
Kämpfe gegen Ausnahmen
Wie bereits erwähnt, muss der Programmiervorgang sicher sein. Dazu müssen verschiedene Nebeneffekte vermieden werden. Ausnahmen sind ihre expliziten Generatoren.
Mit der
Try- Klasse können Sie Ausnahmen in einem funktionalen Stil sicher behandeln. In der Tat ist dies eine typische
Monade . Es ist nicht notwendig, sich mit der Gebrauchstheorie auseinanderzusetzen. Schauen Sie sich ein einfaches Beispiel an:
Try.of(() -> 4 / 0) .onFailure(System.out::println) .onSuccess(System.out::println);
Wie Sie dem Beispiel entnehmen können, ist alles recht einfach. Wir hängen das Ereignis einfach an einen potenziellen Fehler und gehen nicht über die Grenzen der Berechnung hinaus.
Mustervergleich
Oft tritt eine Situation auf, in der wir den Wert einer Variablen überprüfen und das Verhalten des Programms abhängig vom Ergebnis modellieren müssen. Gerade in solchen Situationen hilft eine wunderbare Vorlagensuchmaschine. Sie müssen nicht mehr viel schreiben,
wenn sonst , konfigurieren Sie einfach die gesamte Logik an einem Ort.
import static io.vavr.API.*; import static io.vavr.Predicates.*; public class PatternMatchingDemo { public static void main(String[] args) { String s = Match(1993).of( Case($(42), () -> "one"), Case($(anyOf(isIn(1990, 1991, 1992), is(1993))), "two"), Case($(), "?") ); System.out.println(s);
Bitte beachten Sie, dass Case als aktiviert ist case ist ein Schlüsselwort und wird bereits vergeben.
Fazit
Meiner Meinung nach ist die Bibliothek sehr cool, aber es lohnt sich, sie sehr sorgfältig zu verwenden. Sie kann in
der ereignisgesteuerten Entwicklung großartig
abschneiden . Die übermäßige und gedankenlose Verwendung in der Standardprogrammierung auf der Grundlage eines Thread-Pools kann jedoch viel Kopfzerbrechen verursachen. Darüber hinaus verwenden wir in unseren Projekten häufig Spring und Hibernate, die für eine solche Anwendung nicht immer bereit sind. Bevor Sie eine Bibliothek in Ihr Projekt importieren, müssen Sie genau wissen, wie und warum sie verwendet wird. Worüber ich in einem meiner nächsten Artikel sprechen werde.