1. Begriff der Funktionskomposition
Ein wenig Theorie (aber nicht zu trocken)
In der Mathematik ist Funktionskomposition, wenn das Ergebnis einer Funktion als Eingabe der nächsten dient. Haben wir die Funktionen f und g, bedeutet die Komposition g(f(x)): zuerst wenden wir f auf x an, dann setzen wir das Ergebnis in g ein.
In der Programmierung ist die Idee dieselbe: Wir möchten komplexe Transformationen aus einfachen Bausteinen zusammensetzen, anstatt eine riesige Funktion für alles zu schreiben. Das macht den Code flexibel, wiederverwendbar und gut lesbar.
Stellen Sie sich ein Törtchen-Fließband vor: zuerst der Teig (f), dann die Creme (g), dann die Streusel (h). Der gesamte Prozess ist h(g(f(Zutaten))).
Warum ist Komposition wichtig?
- Komposition erlaubt es, ein Programm aus kleinen Funktions-„Bausteinen“ zusammenzusetzen.
- Wiederverwendung: Einen Baustein kann man an verschiedenen Stellen einsetzen, ohne zu duplizieren.
- Flexibilität: Um einen Schritt zu ändern, ersetzen wir die entsprechende Funktion – der Rest bleibt unberührt.
- Lesbarkeit und Testbarkeit: Kleine Funktionen sind einfacher zu lesen, zu prüfen und zu warten.
2. Die Methoden compose und andThen im Interface Function
Interface Function: kurze Erinnerung
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// Methoden für die Komposition:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
}
- compose: Zuerst wird die Funktion ausgeführt, die Sie an compose übergeben, danach die aktuelle.
- andThen: Zuerst wird die aktuelle Funktion ausgeführt, danach die an andThen übergebene.
Visuelles Schema
// Angenommen, es gibt zwei Funktionen:
Function<String, Integer> parse = s -> Integer.parseInt(s);
Function<Integer, Integer> square = x -> x * x;
// compose: square.compose(parse) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25
// andThen: parse.andThen(square) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25
// Aber die Reihenfolge ist wichtig, wenn die Typen unterschiedlich sind!
Beispiel: String in Zahl und dann zum Quadrat
import java.util.function.Function;
public class ComposeAndThenDemo {
public static void main(String[] args) {
// Funktion: wandelt String in Zahl um
Function<String, Integer> parse = s -> Integer.parseInt(s);
// Funktion: quadriert eine Zahl
Function<Integer, Integer> square = x -> x * x;
// Kombinieren: erst parsen, dann quadrieren
Function<String, Integer> parseThenSquare = parse.andThen(square);
System.out.println(parseThenSquare.apply("7")); // 49
// Und wenn wir vertauschen?
// square.compose(parse) — dasselbe Ergebnis (für diese Funktionen)
Function<String, Integer> squareOfParsed = square.compose(parse);
System.out.println(squareOfParsed.apply("8")); // 64
}
}
Wann spielt die Reihenfolge eine Rolle?
Wenn die Typen der Funktionen nicht zusammenpassen, wird die Reihenfolge kritisch. Zum Beispiel:
Function<String, String> addPrefix = s -> "User: " + s;
Function<String, Integer> length = s -> s.length();
Function<String, Integer> composed = addPrefix.andThen(length);
System.out.println(composed.apply("Alice")); // "User: Alice" -> 11
// Und so:
// length.andThen(addPrefix) — Kompilierfehler!
// length liefert Integer, aber addPrefix erwartet String.
Tabelle: Unterschied zwischen compose und andThen
|
|
|
|
|---|---|---|---|
|
|
|
|
3. Komposition von Prädikaten und anderen Interfaces
Predicate<T>: and, or, negate
Das funktionale Interface Predicate<T> ist eine Funktion, die boolean zurückgibt. Zum Kombinieren von Prädikaten gibt es spezielle Methoden:
- and: logisches UND (&&)
- or: logisches ODER (||)
- negate: logisches NICHT (!)
Beispiel: komplexe Filterbedingungen
Angenommen, wir haben eine Benutzerklasse:
public class User {
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
Nun schreiben wir verschiedene Prädikate:
import java.util.function.Predicate;
Predicate<User> isAdult = user -> user.age >= 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
// Kombinieren: volljährig und Name beginnt mit "A"
Predicate<User> adultAndA = isAdult.and(nameStartsWithA);
// Volljährig oder Name beginnt mit "A"
Predicate<User> adultOrA = isAdult.or(nameStartsWithA);
// Nicht volljährig
Predicate<User> notAdult = isAdult.negate();
Diese Prädikate können wir jetzt beim Filtern verwenden, z. B. mit dem Stream-API:
import java.util.List;
import java.util.stream.Collectors;
List<User> users = List.of(
new User("Alice", 20),
new User("Bob", 17),
new User("Anna", 15),
new User("Mike", 22)
);
List<User> filtered = users.stream()
.filter(adultAndA)
.collect(Collectors.toList());
// In filtered landet nur Alice (volljährig und Name beginnt mit "A")
Komposition von Consumer, Function, Supplier
- Consumer<T>: die Methode andThen ermöglicht es, zwei Operationen hintereinander auszuführen.
- Function<T, R>: die Komposition haben wir oben betrachtet.
- Supplier<T>: lässt sich nicht direkt kombinieren, kann aber innerhalb anderer Funktionen verwendet werden.
Beispiel: Consumer<T>.andThen
import java.util.function.Consumer;
Consumer<String> print = s -> System.out.println("Erhalten: " + s);
Consumer<String> printUpper = s -> System.out.println("In Großbuchstaben: " + s.toUpperCase());
Consumer<String> combined = print.andThen(printUpper);
combined.accept("hello");
// Ausgabe:
// Erhalten: hello
// In Großbuchstaben: HELLO
4. Praxis: Transformations- und Filterketten
Aufgabe 1: Eine Transformationskette Function aufbauen
Angenommen, in unserer Anwendung speichern wir Benutzer als Strings "Name,Alter", zum Beispiel "Alice,20". Es soll:
- Den String in ein User-Objekt umwandeln
- Das Alter ermitteln
- Prüfen, ob der Benutzer volljährig ist
import java.util.function.Function;
import java.util.function.Predicate;
Function<String, User> stringToUser = str -> {
String[] parts = str.split(",");
return new User(parts[0], Integer.parseInt(parts[1]));
};
Function<User, Integer> getAge = user -> user.age;
Predicate<Integer> isAdultAge = age -> age >= 18;
// Kombinieren: String -> User -> Alter -> Prädikat
Function<String, Integer> stringToAge = stringToUser.andThen(getAge);
String input = "Bob,19";
int age = stringToAge.apply(input);
System.out.println("Alter: " + age); // 19
System.out.println("Volljährig? " + isAdultAge.test(age)); // true
Aufgabe 2: Mehrere Predicate für das Filtern kombinieren
Wir möchten Benutzer auswählen, die älter als 18 sind und deren Name mit "A" oder "M" beginnt.
Predicate<User> isAdult = user -> user.age > 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
Predicate<User> nameStartsWithM = user -> user.name.startsWith("M");
Predicate<User> filter = isAdult.and(nameStartsWithA.or(nameStartsWithM));
List<User> filtered = users.stream()
.filter(filter)
.collect(Collectors.toList());
Aufgabe 3: Mehrstufige Transformation Function
Es soll: einen String nehmen, trimmen, in Großbuchstaben umwandeln, den Präfix "USER: " hinzufügen.
Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addPrefix = s -> "USER: " + s;
// Kette zusammenstellen
Function<String, String> pipeline = trim.andThen(toUpper).andThen(addPrefix);
System.out.println(pipeline.apply(" john ")); // USER: VASYA
5. Typische Fehler bei der Funktionskomposition
Fehler Nr. 1: Vertauschte Reihenfolge compose/andThen.
Anfänger verwechseln oft, was zuerst und was danach ausgeführt wird. Merksatz: f.compose(g) — zuerst g, dann f; f.andThen(g) — zuerst f, dann g.
Fehler Nr. 2: Nicht übereinstimmende Typen.
Wenn der Ergebnis-Typ der einen Funktion nicht dem Parameter-Typ der anderen entspricht, lässt der Compiler die Kombination nicht zu. Zum Beispiel kann man nicht Function<Integer, String>.andThen(Function<Double, Boolean>) machen.
Fehler Nr. 3: Zu komplexe Ketten.
Manchmal versucht man, die gesamte Business-Logik in eine einzige Kette zu pressen und erhält Spaghetti-Code. Zerlegen Sie in kleinere Funktionen und geben Sie ihnen aussagekräftige Namen.
Fehler Nr. 4: Nebeneffekte in Funktionen.
Funktionen und Prädikate sollten möglichst „rein“ sein (ohne Nebeneffekte), sonst wird die Komposition riskant und unvorhersehbar.
GO TO FULL VERSION