W3docs

Funktionale Programmierung in Java

Überblick über funktionale Programmierkonzepte in Java — erstklassige Funktionen, Unveränderlichkeit, reine Funktionen und Komposition.

Der vorherige Teil — das Collections Framework — handelte von Containern: Datenstrukturen, die Elemente speichern, und den Operationen (add, remove, iterate, sort, binarySearch), die auf ihnen arbeiten. Dieser Teil behandelt eine andere Ebene desselben Problems. Anstatt wo die Daten leben, werden wir uns darauf konzentrieren, wie Transformationen darauf ausgedrückt werden — klar, kompositional, ohne redundante Schleifen oder Akkumulator-Variablen.

Diese Verschiebung hat einen Namen. Funktionale Programmierung ist der Stil, bei dem Berechnungen als Anwendung von Funktionen auf Werte ausgedrückt werden, bei dem Funktionen selbst erstklassige Werte sind und bei dem Daten in der Regel als unveränderlich behandelt werden. Java wurde nicht als funktionale Sprache entworfen — Klassen, Mutation und explizite Schleifen stehen in ihrem Kern — aber seit Java 8 greift jedes moderne Java-Programm ausgiebig auf den funktionalen Werkzeugkasten zurück. Einiges davon haben Sie bereits im Collections-Teil geschrieben: list.sort(Comparator.comparing(Person::name)), map.getOrDefault(k, 0), List.copyOf(source). Was Teil 12 tut, ist, den Stil explizit zu benennen und Ihnen den Rest der Werkzeuge zu geben — Lambdas, funktionale Interfaces, die java.util.function-Typen, Methodenreferenzen, Optional und die Stream API — damit die Muster, die Sie bisher nachgeahmt haben, zu erstklassigen Techniken werden.

Vier Ideen, die den Stil definieren

Rein funktionale Sprachen (Haskell, Erlang, F#) treiben alle vier bis an ihre Grenzen. Java wendet sie in Maßen an. Die vier Ideen:

  1. Funktionen sind erstklassige Werte. Sie können eine Funktion als Argument übergeben, eine aus einer Methode zurückgeben, eine in einem Feld speichern oder eine zur Laufzeit erstellen.
  2. Reine Funktionen. Eine reine Funktion hängt nur von ihren Eingaben ab und verändert nichts Beobachtbares an der Welt. Bei gleicher Eingabe liefert sie dieselbe Ausgabe. Kein I/O, keine Feldmutation, keine zeitabhängige Verzweigung.
  3. Unveränderlichkeit als Standard. Datenstrukturen werden nicht an Ort und Stelle modifiziert; Transformationen liefern neue Werte zurück. Alte Referenzen bleiben gültig.
  4. Komposition. Größere Funktionen werden durch Kombination kleinerer erstellt (f.andThen(g), pred.and(other), cmp.thenComparing(...)), nicht durch deren Bearbeitung.

Keine dieser Ideen ist Java-spezifisch. Sie sind eine Denkweise, die die Sprache nun durch Lambdas, Methodenreferenzen, die Stream API und die unveränderlichen Collections, die Sie gerade kennengelernt haben, unterstützt.

1. Funktionen als Werte

Vor Java 8 konnte man keine Variable haben, deren Wert eine Funktion war. Man konnte ein Objekt übergeben, dessen Klasse zufällig eine Methode hatte — das waren Runnable, Comparator und ActionListener — aber die Syntax war umständlich:

list.sort(new Comparator<String>() {
  @Override
  public int compare(String a, String b) {
    return a.length() - b.length();
  }
});

Die einzelne Methode war in einer anonymen Klassendeklaration verpackt. Java 8 führte Lambda-Ausdrücke als kompakte Syntax für dieselbe Idee ein:

list.sort((a, b) -> a.length() - b.length());

Der Lambda ist der Wert. Er kompiliert zu einer Instanz des jeweiligen funktionalen Interfaces, das an der Aufrufstelle benötigt wird (hier Comparator<String>). Das nächste Kapitel behandelt die Syntax vollständig; vorerst ist der wichtige Punkt, dass Funktionen in Java nun Werte sind, die man benennen, speichern und übergeben kann.

2. Reine Funktionen

Eine reine Funktion ist eine, deren Rückgabewert nur von ihren Argumenten abhängt und deren Ausführung keine beobachtbaren Nebeneffekte hat. Math.sqrt(2) ist rein. System.currentTimeMillis() ist es nicht — es gibt bei verschiedenen Aufrufen unterschiedliche Werte zurück. list.add(x) ist es nicht — es mutiert list.

Reine Funktionen sind wertvoll, weil:

  • Sie leicht zu testen sind — kein Setup, keine Mocks, nur assertEquals(expected, f(input)).
  • Sie leicht parallelisierbar sind — zwei reine Aufrufe können auf verschiedenen Threads ohne Synchronisation laufen.
  • Sie leicht zu cachen sind — einmal memoizieren, für immer dieselbe Antwort zurückgeben.
  • Sie ohne Überraschungen komponierenf(g(x)) tut das, was man beim Lesen erwartet.

Die meisten nützlichen realen Programme sind nicht zu 100 % rein (jemand muss in eine Datenbank schreiben). Die funktionale Disziplin besteht darin, die Kernberechnung rein zu halten und die unreinen Teile — I/O, Zeit, Zufälligkeit, Mutation — an die Ränder zu verlagern. Streams fördern dies: eine Pipeline aus reinen Operationen ist von Natur aus korrekt; eine unreine (stream().peek(x -> counter++)...) ist fehleranfällig.

3. Unveränderlichkeit

Das haben Sie im letzten Kapitel kennengelernt. List.of(...), Set.of(...), Map.of(...) und List.copyOf(...) erzeugen Collections, die nicht verändert werden können. Records (später behandelt) geben Ihnen unveränderliche Datenklassen:

record Point(double x, double y) {
  Point translated(double dx, double dy) {
    return new Point(x + dx, y + dy);     // returns a NEW Point — does not mutate this
  }
}

Unveränderliche Werte sind von Natur aus thread-sicher. Sie zeigen nie einen "zerrissenen" Zwischenzustand. Sie können frei geteilt werden, ohne defensive Kopien zu erstellen. Und sie machen reine Funktionen praktisch — wenn Werte sich nicht ändern können, ist eine Funktion, die einen zurückgibt, garantiert deterministisch für diesen Teil der Welt.

4. Komposition

Komposition bedeutet "eine große Funktion aus kleinen aufbauen." In Java bieten Function, Predicate und Comparator alle kompositionelle Operatoren:

Function<String, String> trim  = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> clean = trim.andThen(upper);   // trim, then upper

Predicate<Integer> positive = n -> n > 0;
Predicate<Integer> even     = n -> n % 2 == 0;
Predicate<Integer> posEven  = positive.and(even);

Comparator<String> byLength    = Comparator.comparingInt(String::length);
Comparator<String> lengthThenA = byLength.thenComparing(Comparator.naturalOrder());

Die kompositionelle API ist Teil des Wertes. Sie schreiben keine Hilfsmethode, die zwei Prädikate nimmt und sie mit && verknüpft — Sie schreiben a.and(b). Der Stil skaliert: eine sechsstufige Transformation kann von oben nach unten als ein einziger Ausdruck gelesen werden, anstatt als sechs verschachtelte Schleifen mit Zwischenakkumulatoren.

Was Java von der imperativen Seite behält

Java ist multiparadigmatisch. Die in Java 8+ hinzugefügten funktionalen Features existieren neben den imperativen Features, die seit 1.0 vorhanden sind. Einige Dinge bleiben absichtlich imperativ:

  • Anweisungen und Kontrollfluss. if, for, while, try sind weiterhin die grundlegenden Bausteine; Lambdas ersetzen sie nicht, sie ersetzen anonyme Klassen-Boilerplate.
  • Veränderliche lokale Variablen. Innerhalb eines Methodenrumpfs ist int sum = 0; for (int x : xs) sum += x; nach wie vor idiomatisch.
  • Veränderliche Felder, wo sie sinnvoll sind. Builder, Caches und zustandsbehaftete UI-Komponenten mutieren weiterhin.

Das Prinzip: Verwenden Sie funktionalen Stil, wo er den Code klarer macht, nicht als Dogma. Ein reines stream().mapToInt(Integer::intValue).sum() ist klarer als eine handgestrickte Schleife. Eine sechsstufige Lambda-Kompositions-Pipeline, die niemand in Ihrem Team lesen kann, ist es nicht.

Ein ausgearbeitetes Beispiel: imperativ vs. funktional, nebeneinander

Das folgende Programm berechnet die durchschnittliche Länge der nicht-leeren Strings in einer Liste, zweimal. Die erste Version ist imperativ — ein veränderlicher Akkumulator, eine explizite Schleife, ein Schutz gegen Division durch null. Die zweite Version ist funktional — eine Stream-Pipeline aus reinen Operationen, die von oben nach unten gelesen werden kann. Das dritte Snippet erstellt zusammengesetzte Predicate- und Function-Werte aus kleineren Werten und zeigt Komposition in Aktion.

java— editable, runs on the server

Was aus dem Ausführungsergebnis mitgenommen werden sollte:

  • Beide Versionen berechnen denselben Durchschnitt. Die imperative Version deklariert zwei veränderliche Zähler und einen Schleifenrumpf; die funktionale Version verkettet fünf benannte Operationen, die jeweils beschreiben was, nicht wie.
  • Predicate.and hat einen zusammengesetzten Test (notNull.and(notBlank)) aus zwei kleineren Prädikaten erstellt — keine neue Hilfsmethode nötig. Das ist Komposition in Aktion.
  • Function.andThen hat dasselbe für eine wertproduzierende Pipeline getan: trim dann length, ausgedrückt als eine zusammengesetzte Function<String, Integer>.
  • Jede Operation im Stream ist rein: String::trim, das Lambda s -> !s.isEmpty(), String::length — keine mutiert den Zustand. trimmedLen.apply(\" hi \") zweimal aufzurufen, lieferte dieselbe Antwort; das ist die Determinismus-Garantie, die reine Funktionen sicher für Memoization und Parallelisierung macht.

Was als Nächstes kommt

Das mentale Modell ist nun vorhanden: Funktionen sind Werte, reine Transformationen komponieren, Unveränderlichkeit befreit Sie von einer Klasse von Fehlern. Das nächste Kapitel, Java Lambda Expressions, führt die konkrete Syntax ein — (params) -> body — die diesen Stil in Java ergonomisch macht, plus die Regeln rund um Variablenerfassung, Zieltypisierung und wo ein Lambda erscheinen kann.

Übungen

Übung
Eine 'reine' Funktion im Sinne der funktionalen Programmierung ist eine, die...
Eine 'reine' Funktion im Sinne der funktionalen Programmierung ist eine, die...
Was this page helpful?