Java Function Interface
Einen Wert von einem Typ in einen anderen umwandeln mit dem Function-Interface und andThen/compose.
Function<T, R> ist das funktionale Interface für die Frage "Wandle dieses T in ein R um" — eine Eingabe, eine Ausgabe, keine Seiteneffekte erwartet. Es ist die Form, die Stream.map annimmt, die Form, die Optional.map annimmt, die Form, die Map.computeIfAbsent annimmt, und die Form, die jede JDK-Methode akzeptiert, die "transformiere dies in jenes" bedeutet. Eine einzige abstrakte Methode, drei oder vier nützliche Default-Methoden und eine kleine Algebra (andThen, compose, identity) zum Verknüpfen von Transformationen, ohne Zwischen-Lambdas schreiben zu müssen.
Das Interface
@FunctionalInterface
public interface Function<T, R> {
R apply(T t); // the only abstract method
default <V> Function<V, R> compose(Function<? super V, ? extends T> before);
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after);
static <T> Function<T, T> identity();
}apply(T) ist die SAM (single abstract method). Jedes Lambda oder jeder Methodenverweis, der an einer Function<T, R>-Position landet, implementiert sie.
Function<String, Integer> length = String::length;
int n = length.apply("hello"); // 5Normalerweise lässt man stream.map(length) oder optional.map(length) apply für sich aufrufen. Der Methodenname ist wichtig, wenn man Code schreibt, der eine Function<T, R> entgegennimmt und sie einmal aufrufen muss.
andThen und compose — zwei Wege zum Verketten
Die beiden Default-Methoden erstellen eine neue Function, indem sie den Empfänger mit einer anderen verknüpfen. Sie unterscheiden sich nur in der Richtung:
Function<String, String> trim = String::trim;
Function<String, Integer> length = String::length;
Function<String, Integer> trimThenLength = trim.andThen(length); // f.andThen(g): g(f(x))
Function<String, Integer> sameThing = length.compose(trim); // g.compose(f): g(f(x))Beide erzeugen die gleiche Pipeline s -> length(trim(s)). Der Unterschied liegt darin, welche sich an der Aufrufstelle besser liest:
andThenliest sich von links nach rechts, in derselben Reihenfolge, in der die Daten fließen.trim.andThen(length).andThen(asString)bedeutet "trimmen, dann Länge, dann asString."composeliest sich von rechts nach links, so wie mathematische Komposition geschrieben wird:f ∘ gbedeutet "wendegzuerst an, dannf."length.compose(trim)bedeutet "length nach trim."
Im Anwendungscode ist andThen fast immer die klarere Wahl — Code liest sich von oben nach unten, von links nach rechts, und eine links-nach-rechts-Pipeline passt dazu. compose ist nützlich, wenn man eine abschließende Funktion hat und Vorverarbeitung voranstellen möchte, ohne die Kette neu zu schreiben.
Beide sind in dem Sinne lazy, dass sie beim Kompositionszeitpunkt nichts ausführen; sie produzieren lediglich eine neue Function, deren apply die zugrunde liegenden in der richtigen Reihenfolge aufruft.
Function.identity() — die Keine-Operation-Transformation
Function<T, T> id = Function.identity(); // t -> tidentity() gibt bei jedem Aufruf dieselbe Instanz zurück (ein Singleton-Lambda), sodass es keine Allokationskosten hat. Die einzige Stelle, an der es sich bewährt, ist als Schlüssel- oder Wert-Mapper in Collectors.toMap, wo man eine Function übergeben muss, auch wenn der Wert "das Element selbst" ist:
Map<String, Person> byName = people.stream()
.collect(Collectors.toMap(Person::name, Function.identity())); // key=name, value=personOhne Function.identity() würde man p -> p schreiben, was bei jedem Aufruf ein neues Lambda allokiert und sich schlechter liest.
Ein subtiler Punkt: identity() funktioniert nur, wenn Eingabe- und Ausgabetyp identisch sind. Sobald ein generischer Typ erweitert wird (Function<? super T, ? extends R>), kann der Compiler einen dazu zwingen, erneut ein Lambda auszuschreiben. Das ist ein Randfall, aber es lohnt sich, ihn zu kennen, wenn generische Typinferenz sich beschwert.
Function<T, R> versus UnaryOperator<T>
UnaryOperator<T> ist die Spezialisierung für den Fall, dass Eingabe und Ausgabe denselben Typ haben:
UnaryOperator<String> upper = String::toUpperCase; // String -> String
Function<String, String> sameShape = String::toUpperCase;Beide sind gültige Function<String, String>-Instanzen — UnaryOperator<T> erweitert Function<T, T>. Der Unterschied liegt auf API-Ebene: List.replaceAll, Map.replaceAll und Comparator.thenComparing(UnaryOperator) deklarieren UnaryOperator<T>, weil "ersetze jedes Element durch einen transformierten Wert desselben Typs" genau diese Form hat. Übergibt man einen Methodenverweis, wählt der Compiler den richtigen.
BiFunction<T, U, R> — zwei Eingaben
Die Zwei-Argument-Form:
BiFunction<String, Integer, String> repeat = String::repeat;
String s = repeat.apply("ab", 3); // "ababab"BiFunction hat dasselbe andThen, aber kein compose — die Asymmetrie ist beabsichtigt, weil das Vorverarbeiten einer Zwei-Argument-Funktion zwei compose-Parameter erfordern würde.
Das JDK verwendet BiFunction<K, V, V> für Map.merge und BiFunction<K, V, V_NEW> für Map.compute. BinaryOperator<T> ist der Sonderfall, bei dem alle drei Typparameter T sind (Eingabe, Eingabe und Ausgabe alle gleich) — behandelt im BinaryOperator-Kapitel.
Primitive Spezialisierungen — drei Familien
Function<Integer, String> boxed das int bei jedem Aufruf. Das Paket liefert drei Familien, um das zu vermeiden:
// 1. Primitive in, object out — "IntFunction<R>"
IntFunction<String> fromInt = i -> "n=" + i;
// 2. Object in, primitive out — "ToIntFunction<T>"
ToIntFunction<String> strLen = String::length;
ToDoubleFunction<Item> price = Item::price;
// 3. Primitive in, primitive out — "IntToLongFunction", "IntUnaryOperator", etc.
IntToLongFunction square = i -> (long) i * i;
IntUnaryOperator doubleIt = i -> i * 2;
DoubleUnaryOperator halve = d -> d / 2.0;Die Benennung liest sich wie ein Satz:
IntX— operiert auf einemint.ToIntX— produziert einint.IntToLongX—intrein,longraus.
Stream.mapToInt(ToIntFunction) ist die Brücke von einem gebündelten Stream<T> in einen IntStream. Sobald man auf einem IntStream ist, verwendet jede Transformation IntUnaryOperator oder IntToLongFunction — und die Boxing-Kosten bleiben bei null.
Ein ausgearbeitetes Beispiel: Komposition, Identity und eine primitive Spezialisierung
Das Programm unten erstellt zwei Functions, verknüpft sie mit sowohl andThen als auch compose, um zu zeigen, dass sie äquivalent sind, verwendet Function.identity() innerhalb eines Collectors.toMap und stellt eine gebündelte Function<Integer, Integer> einer primitiven IntUnaryOperator bei einer ausreichend großen Arbeitslast gegenüber, um die Boxing-Kosten zu spüren.
Was man aus dem Lauf mitnehmen kann:
trim.andThen(upper)undupper.compose(trim)produzierten denselbenStringaus derselben Eingabe. Sie unterscheiden sich nur darin, welcher Name natürlich liest, wo man ihn schreibt —andThenpasst zum links-nach-rechts-Datenfluss,composepasst zur mathematischen "f nach g"-Notation.- Die längere Kette
trim.andThen(upper).andThen(length)änderte den Ausgabetyp vonStringzuIntegerunterwegs. Die Pipeline setzt sich typsicher zusammen; der Compiler hatString -> String -> String -> Integerfür einen nachverfolgt. Function.identity()passte als Wert-Mapper inCollectors.toMap(Person::name, Function.identity()). Das Lambdap -> phätte funktioniert, aberidentity()ist die Singleton-Variante ohne Allokation und liest sich als Absicht ("der Wert ist die Person").- Die gebündelte
Function<Integer, Integer>zahlt bei jedem Aufruf für zweiInteger-Boxings; der primitiveIntUnaryOperatorzahlt nichts. Ein einzelner aufgewärmter Lauf kann ähnliche Zeiten zeigen — der JIT ist gut darin, kurzlebige Boxes zu eliminieren — aber unter realem Allokationsdruck (große Heaps, gleichzeitige GC, entweichende Werte) ist die primitive Variante die, die standhält. Greife darauf in heißen Pipelines zurück, die Millionen von Werten verarbeiten. BiFunction.andThen(Function)verknüpfte eine Zwei-Argument-Funktion mit einer Ein-Argument-Fortsetzung. Es gibt keinBiFunction.compose— das Vorverarbeiten von zwei Eingaben würde zweicompose-Argumente erfordern, was die API bewusst vermeidet.
Was kommt als Nächstes
Function<T, R> und Predicate<T> sind beide reine Formen — Eingabe, Ausgabe, keine erwarteten Seiteneffekte. Das nächste Kapitel, Java Consumer und Supplier, behandelt die beiden Interfaces, die über diese Reinheit hinausgehen: Consumer<T> nimmt eine Eingabe und produziert nichts (einen Seiteneffekt — drucken, loggen, speichern), und Supplier<T> nimmt nichts und produziert eine Ausgabe (lazy Default, Factory, Zufälligkeit). Sie vervollständigen die Vier-Ecken-Taxonomie, die man in der Übersicht der eingebauten Interfaces gesehen hat.