Java Functional Interfaces
Interfaces mit genau einer abstrakten Methode in Java, die als Ziel für Lambdas dienen und mit @FunctionalInterface markiert werden.
Ein funktionales Interface ist ein Interface mit genau einer abstrakten Methode. Genau diese eine Methode ist das Ziel, auf das ein Lambda oder eine Methodenreferenz kompiliert wird. Runnable, Comparator<T>, Callable<V>, Supplier<T>, Function<T, R>, Predicate<T>, Consumer<T>, ActionListener, FileFilter — all das sind funktionale Interfaces. Im JDK gibt es bereits Dutzende davon, und Sie werden eigene schreiben, wenn keines davon passt.
Das vorherige Kapitel zeigte Lambdas wie () -> 42 und s -> s.length(), die „auf das Interface kompilieren, das der Kontext benötigt." Dieses Kapitel beantwortet die Frage, was ein Interface zu einem gültigen Ziel macht — die Single-Abstract-Method-Regel (SAM) — und wie @FunctionalInterface es Ihnen ermöglicht zu sagen: „Ja, das ist eines, und ich möchte, dass der Compiler es durchsetzt."
Die SAM-Regel, genau betrachtet
Um funktional zu sein, muss ein Interface genau eine Methode deklarieren, die eine Implementierung benötigt. Die Formulierung ist wichtig: nicht „genau eine Methode insgesamt", sondern „genau eine abstrakte Methode". Drei Kategorien von Methoden zählen nicht zu dieser einen:
default-Methoden — sie haben bereits einen Rumpf, sodass eine implementierende Klasse keinen bereitstellen muss.static-Methoden — sie gehören zum Interface selbst, nicht zu den Implementierern.publicabstrakte Methoden, die eine Methode vonjava.lang.Objectüberschreiben — z. B.equals,hashCode,toString. Jede Klasse erbt bereits Implementierungen vonObject, daher fügt deren Neudeklaration in einem Interface keine neue Anforderung hinzu.
Der dritte Punkt überrascht viele. Comparator<T> deklariert boolean equals(Object), ist aber trotzdem funktional, weil diese Methode von Object stammt. Die eigentliche abstrakte Methode ist int compare(T, T).
@FunctionalInterface
interface MyComparator<T> {
int compare(T a, T b); // the one SAM
boolean equals(Object other); // Object override — doesn't count
default MyComparator<T> reversed() { // default — doesn't count
return (a, b) -> compare(b, a);
}
static <T extends Comparable<T>> MyComparator<T> natural() { // static — doesn't count
return (a, b) -> a.compareTo(b);
}
}@FunctionalInterface — optionale Kompilierzeit-Prüfung
Die Annotation ist optional. Ein Interface ist funktional aufgrund seiner Struktur, nicht weil es annotiert ist. Die Annotation bringt jedoch zwei Vorteile:
- Kompilierfehler, wenn das Interface aufhört funktional zu sein. Wird versehentlich eine zweite abstrakte Methode hinzugefügt, stoppt der Compiler sofort — am Interface selbst, nicht an jeder Aufrufstelle, die es als Lambda-Ziel verwendet.
- Dokumentation. Die Annotation signalisiert „dieses Interface ist als Lambda-Ziel gedacht", was bei jedem nicht offensichtlichen Fall den Wert hat, explizit ausgedrückt zu werden.
@FunctionalInterface
interface Validator<T> {
boolean isValid(T value);
boolean isInvalid(T value); // <-- compile error: not a functional interface
}Ohne die Annotation würde die zweite Methode Validator<T> stillschweigend zu einem nicht-funktionalen Interface machen, und die erste Lambda-Aufrufstelle, die es verwendet, würde mit einer verwirrenden Meldung weit von der Ursache entfernt scheitern.
Die Annotation ist auch die Konvention für die eigenen funktionalen Interfaces des JDK — Function, Predicate, Consumer, Supplier, Runnable, Callable tragen sie alle.
Lambdas, Methodenreferenzen und anonyme Klassen sind austauschbar
Ein funktionales Interface akzeptiert drei Arten von Werten, die frei austauschbar sind:
Predicate<String> blank1 = s -> s.trim().isEmpty(); // lambda
Predicate<String> blank2 = String::isBlank; // method reference (since Java 11)
Predicate<String> blank3 = new Predicate<>() { // anonymous class
@Override public boolean test(String s) { return s.trim().isEmpty(); }
};Alle drei implementieren dasselbe Predicate<String>-Interface und erzeugen äquivalente Werte an der Aufrufstelle. Das Lambda und die Methodenreferenz sind deutlich kürzer; die anonyme Klasse ist für die seltenen Fälle reserviert, die im vorherigen Kapitel aufgeführt wurden (mehr als eine Methode erforderlich, methodenlokaler Zustand, this bezieht sich auf die neue Instanz).
Generische funktionale Interfaces
Das Interface kann parametrisiert werden — so kann eine einzige Deklaration von Function<T, R> für jede Transformation verwendet werden:
@FunctionalInterface
interface Mapper<T, R> {
R map(T input);
}
Mapper<String, Integer> length = s -> s.length();
Mapper<Integer, String> hex = n -> Integer.toHexString(n);Die Parameter können begrenzt sein, mehrere Typvariablen enthalten und über Interfaces hinweg wiederverwendet werden — die Standardbibliothek nutzt jede Variante.
Eigene funktionale Interfaces schreiben
Meistens sollten Sie die integrierten Interfaces in java.util.function verwenden — das nächste Kapitel geht sie alle durch. Schreiben Sie eigene, wenn:
- Die Semantik einen Namen verdient.
Validator<T>ist an einer Aufrufstelle besser lesbar alsFunction<T, ValidationResult>, auch wenn die Struktur übereinstimmt. - Sie eine geprüfte Ausnahme benötigen.
Function.applywirft keine geprüften Ausnahmen; wenn Ihre OperationIOExceptionwirft, schreiben Sie eine SAM, die sie deklariert. - Die Struktur nicht in der Standardbibliothek vorhanden ist. Eine Methode, die drei Argumente akzeptiert (eine Tri-Funktion), hat kein integriertes Interface — schreiben Sie eines, wenn Sie es benötigen.
@FunctionalInterface
interface IOFunction<T, R> {
R apply(T input) throws IOException;
}
IOFunction<Path, String> readAll = Files::readString; // declared exception — built-in Function can'tErstaunlich viele Überlegungen, ob man ein Interface schreiben sollte, laufen auf Lesbarkeit oder Ausnahmenweiterleitung hinaus.
Default-Methoden zahlen sich aus
Der einzige Fall, in dem Sie ein eigenes funktionales Interface schreiben und default-Methoden hinzufügen, ist, wenn Sie möchten, dass Aufrufer Instanzen zusammensetzen können:
@FunctionalInterface
interface Filter<T> {
boolean keep(T value);
default Filter<T> and(Filter<T> other) {
return v -> keep(v) && other.keep(v);
}
default Filter<T> negate() {
return v -> !keep(v);
}
}
Filter<Integer> positive = n -> n > 0;
Filter<Integer> even = n -> n % 2 == 0;
Filter<Integer> posOdd = positive.and(even.negate());Das ist genau das Rezept, das das JDK für Predicate.and / or / negate, Function.andThen / compose und Comparator.thenComparing verwendet. Die einzelne abstrakte Methode ist das Verhalten; die Default-Methoden sind die Kompositionsalgebra, die es umgibt.
Ein ausgearbeitetes Beispiel: schreiben, annotieren, zusammensetzen
Das folgende Programm definiert ein funktionales Interface Filter<T> mit zwei default-Methoden, demonstriert die SAM-Regel (eine zusätzliche abstrakte Methode würde nicht kompilieren) und zeigt Lambdas, Methodenreferenzen und eine anonyme Klasse, die alle dieselbe SAM implementieren.
Was aus der Ausführung mitgenommen werden sollte:
notBlank1(Lambda),notBlank2(Methodenreferenz-Kette) undnotBlank3(anonyme Klasse) implementieren alle dasselbeFilter<String>-Interface — austauschbar. Das Lambda ist am kürzesten; die anonyme Klasse ist für Fälle reserviert, die Lambdas nicht handhaben können.positive.and(even.negate())setzte drei Filter ohne zusätzliche Methodendeklarationen zu einem zusammen. Diedefault-Methodenandundnegateam Interface sind die Kompositionsalgebra — deshalb fügt das JDK sie auch zuPredicate,FunctionundComparatorhinzu.SafelyFunctional<T>deklariert sowohlapply(T)als auchboolean equals(Object)und wurde trotzdem mit@FunctionalInterfacekompiliert. Dieequals-Überschreibung wird vonObjectgeerbt und zählt daher nicht gegen die Single-Abstract-Method-Regel.- Wenn Sie das Schlüsselwort
defaultinFilterentfernen (und eine Default-Methode in eine zweite abstrakte Methode verwandeln), erzwingt die@FunctionalInterface-Annotation sofort einen Kompilierfehler an der Interface-Deklaration — lange bevor eine Lambda-Aufrufstelle verwirrende Inferenzfehler sieht.
Wie es weitergeht
Sie können ein funktionales Interface erkennen, eines schreiben, wenn das JDK nicht das Richtige anbietet, und den Compiler seine Form durchsetzen lassen. Fast immer ist die richtige Antwort jedoch „verwenden Sie, was bereits vorhanden ist." Das nächste Kapitel, Java Built-in Functional Interfaces, führt durch java.util.function — Function, Predicate, Consumer, Supplier, ihre Bi-Varianten und die primitiven Spezialisierungen, die existieren, um Boxing zu vermeiden.