Eingebaute funktionale Interfaces in Java
Das java.util.function-Paket — Function, Predicate, Consumer, Supplier und ihre spezialisierten Varianten.
Das java.util.function-Paket wurde mit Java 8 ausgeliefert, um dem JDK — und Ihrem Code — ein gemeinsames Vokabular für Lambdas zu geben. Ohne es müsste jede Methode, die eine Funktion akzeptiert, ihr eigenes einmaliges Interface definieren (StringMapper, IntToBool, RowHandler, …), und für ein Interface definierte Lambdas wären für ein anderes nicht wiederverwendbar. Das Paket löst das mit 43 kleinen Interfaces, die die Formen abdecken, die immer wieder auftauchen: "nimm eine Sache, gib eine andere zurück," "nimm eine Sache, entscheide ja oder nein," "nimm eine Sache, tue etwas," "gib mir eine Sache."
Wenn Sie nur vier Interfaces aus diesem Paket lernen, lernen Sie Function, Predicate, Consumer und Supplier. Fast alles andere ist eine Variante davon — Zweiargument-Versionen, primitive Spezialisierungen zum Vermeiden von Boxing oder Kompositions-Helfer.
Die vier großen
Function<T, R> f = t -> ...; // takes a T, returns an R — r = f.apply(t)
Predicate<T> p = t -> ...; // takes a T, returns a boolean — boolean b = p.test(t)
Consumer<T> c = t -> { ... }; // takes a T, returns nothing — c.accept(t)
Supplier<T> s = () -> ...; // takes nothing, returns a T — t = s.get()Jedes ist mit @FunctionalInterface annotiert und hat eine einwörtige abstrakte Methode (apply, test, accept, get). Sie werden diese Methoden selten direkt aufrufen, wenn Streams im Spiel sind — stream().filter(predicate).map(function).forEach(consumer) übernimmt den Aufruf für Sie — aber den Methodennamen zu kennen ist wichtig, wenn Sie Code schreiben, der eine Function<T, R> als Parameter entgegennimmt und sie aufrufen muss.
Die Formen entsprechen häufigen Fragen:
| Frage | Interface |
|---|---|
| "Ein X in ein Y umwandeln?" | Function<X, Y> |
| "Ist dieses X gut?" | Predicate<X> |
| "Etwas mit diesem X tun" | Consumer<X> |
| "Gib mir ein X" | Supplier<X> |
Zweiargument-Varianten
Wenn die Operation zwei Eingaben benötigt, fügen Sie das Bi-Präfix hinzu:
BiFunction<T, U, R> f = (t, u) -> ...; // two ins, one out — apply
BiPredicate<T, U> p = (t, u) -> ...; // two ins, a boolean — test
BiConsumer<T, U> c = (t, u) -> { ... }; // two ins, no out — acceptEs gibt kein BiSupplier — Supplier nimmt per Definition null Argumente entgegen, sodass ein "Zweiargument-Supplier" einfach eine BiFunction wäre.
Die Bi-Varianten sind genau das, was Map.forEach((k, v) -> ...), Map.merge und Map.compute erwarten:
Map<String, Integer> scores = new HashMap<>();
scores.forEach((name, score) -> System.out.println(name + "=" + score)); // BiConsumer
scores.merge("alice", 1, Integer::sum); // BinaryOperator<Integer>BinaryOperator<T> ist eine BiFunction<T, T, T> — gleicher Typ für beide Eingaben und die Ausgabe. UnaryOperator<T> ist entsprechend eine Function<T, T>.
Primitive Spezialisierungen — die Boxing-Steuer vermeiden
Function<Integer, Integer> funktioniert, aber jeder Aufruf boxt die Eingabe und boxt das Ergebnis. In einer engen Schleife ist das ein echter Kostenfaktor. Das Paket bietet daher primitiv spezialisierte Versionen an:
IntFunction<R> f = i -> ...; // int in, R out
IntPredicate p = i -> ...; // int in, boolean out
IntConsumer c = i -> { ... }; // int in, void
IntSupplier s = () -> 42; // void in, int out
IntUnaryOperator u = i -> i * 2; // int in, int out
IntBinaryOperator b = (a, c2) -> a + c2;
ToIntFunction<T> f1 = t -> t.hashCode(); // T in, int out
ToIntBiFunction<T, U> f2 = (t, u) -> t.hashCode() + u.hashCode();
IntToLongFunction f3 = i -> (long) i * i; // int in, long out
IntToDoubleFunction f4 = i -> Math.sqrt(i);Dieselbe Familie existiert für Long und Double. Die Namenskonvention liest sich wie ein Satz:
IntX— operiert auf einemint.ToIntX— produziert einint.IntToLongX—intrein,longraus.
In Stream-Code gibt mapToInt(...) einen IntStream zurück, dessen Terminaloperationen (sum, average, min, max) alle Primitive ohne Boxing zurückgeben — was einer der größten praktischen Vorteile der primitiven Varianten ist.
In die Interfaces eingebaute Komposition
Die meisten Interfaces werden mit default-Methoden geliefert, mit denen Sie ohne neue Lambdas komponieren können:
// Function: andThen (left-to-right), compose (right-to-left)
Function<String, String> trim = String::trim;
Function<String, Integer> len = String::length;
Function<String, Integer> trimLen = trim.andThen(len); // trim, then length
Function<String, Integer> sameThing = len.compose(trim); // length applied after trim
// Predicate: and / or / negate
Predicate<String> notNull = Objects::nonNull;
Predicate<String> notBlank = s -> !s.trim().isEmpty();
Predicate<String> useful = notNull.and(notBlank);
Predicate<String> blank = notBlank.negate();
// Consumer: andThen (run two consumers in sequence)
Consumer<String> log = System.out::println;
Consumer<String> save = s -> writeToFile(s);
Consumer<String> both = log.andThen(save);
// Comparator (in java.util, not java.util.function, but the same idea):
Comparator<Person> byName = Comparator.comparing(Person::name);
Comparator<Person> ordered = byName.thenComparing(Person::age);Es gibt auch eine nützliche statische Factory: Predicate.not(p) ist eine Kurzform für p.negate() und liest sich an einer Aufrufstelle natürlicher:
list.removeIf(Predicate.not(String::isBlank)); // remove all blank stringsFunction.identity und Predicate.isEqual — die kleinen nützlichen Statiken
Zwei Factory-Methoden, die Sie in Stream-Code sehen werden und erkennen sollten:
Function<T, T> id = Function.identity(); // t -> t — useful as a no-op map
Predicate<Object> isFoo = Predicate.isEqual("foo"); // o -> Objects.equals(o, "foo")Function.identity() wird am häufigsten als Schlüssel- oder Wert-Mapper in Collectors.toMap verwendet:
Map<String, Person> byName = people.stream()
.collect(Collectors.toMap(Person::name, Function.identity()));Predicate.isEqual ist selten kürzer als s -> s.equals("foo"), aber es vergleicht null-sicher mit Objects.equals, was wichtig ist, wenn der Stream null enthalten kann.
Ein ausgearbeitetes Beispiel: die vier großen, Komposition und primitive Spezialisierung
Das folgende Programm verwendet Function, Predicate, Consumer und Supplier, komponiert einige davon und vergleicht eine Function<Integer, Integer> (geboxt) mit einem IntUnaryOperator (primitiv), indem es eine kleine Liste summiert.
Was man aus dem Lauf mitnehmen kann:
- Die vier großen Interfaces bilden sauber auf vier Arten von Arbeit ab: transformieren (
Function), testen (Predicate), handeln (Consumer), produzieren (Supplier). Ihre abstrakten Methodennamen (apply,test,accept,get) sind es wert, auswendig gelernt zu werden. trim.andThen(length)undnotNull.and(notBlank)bauten neue Werte aus alten, ohne Hilfs-Methodendeklarationen. Das ist die Kompositionsalgebra, die die Interfaces alsdefault-Methoden mitbringen.- Die geboxte
Function<Integer, Integer>ist merklich langsamer als der primitiveIntUnaryOperator, weil jeder Aufruf zweiInteger-Objekte alloziert. In heißen Pfaden — Stream-Pipelines, die Millionen von Werten verarbeiten — lohnen sich die primitiven Spezialisierungen. Predicate.not(notBlank)liest sich an einerremoveIf-Aufrufstelle natürlicher alsnotBlank.negate(). Beide kompilieren zum Gleichen.
Was kommt als Nächstes
Sie haben jetzt das Standardvokabular kennengelernt. Die verbleibende Frage zu Lambda-Ergonomie lautet: "Wenn der Lambda-Körper nur an eine existierende Methode delegiert, kann ich ihn kürzer schreiben?" Ja — mit Methoden-Referenzen. Das nächste Kapitel, Java Method References, behandelt den :: Operator und seine vier Formen (statisch, gebundene Instanz, ungebundene Instanz, Konstruktor) und erklärt, wann eine Methoden-Referenz klarer als ein Lambda ist und wann das Gegenteil der Fall ist.