W3docs

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:

  1. default-Methoden — sie haben bereits einen Rumpf, sodass eine implementierende Klasse keinen bereitstellen muss.
  2. static-Methoden — sie gehören zum Interface selbst, nicht zu den Implementierern.
  3. public abstrakte Methoden, die eine Methode von java.lang.Object überschreiben — z. B. equals, hashCode, toString. Jede Klasse erbt bereits Implementierungen von Object, 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:

  1. 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.
  2. 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 als Function<T, ValidationResult>, auch wenn die Struktur übereinstimmt.
  • Sie eine geprüfte Ausnahme benötigen. Function.apply wirft keine geprüften Ausnahmen; wenn Ihre Operation IOException wirft, 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't

Erstaunlich 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.

java— editable, runs on the server

Was aus der Ausführung mitgenommen werden sollte:

  • notBlank1 (Lambda), notBlank2 (Methodenreferenz-Kette) und notBlank3 (anonyme Klasse) implementieren alle dasselbe Filter<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. Die default-Methoden and und negate am Interface sind die Kompositionsalgebra — deshalb fügt das JDK sie auch zu Predicate, Function und Comparator hinzu.
  • SafelyFunctional<T> deklariert sowohl apply(T) als auch boolean equals(Object) und wurde trotzdem mit @FunctionalInterface kompiliert. Die equals-Überschreibung wird von Object geerbt und zählt daher nicht gegen die Single-Abstract-Method-Regel.
  • Wenn Sie das Schlüsselwort default in Filter entfernen (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.functionFunction, Predicate, Consumer, Supplier, ihre Bi-Varianten und die primitiven Spezialisierungen, die existieren, um Boxing zu vermeiden.

Übungen

Übung
Ein Interface deklariert drei Methoden: eine abstrakte Methode, eine `default`-Methode und `boolean equals(Object)`, das von `Object` neu deklariert wird. Ist es ein gültiges `@FunctionalInterface`?
Ein Interface deklariert drei Methoden: eine abstrakte Methode, eine `default`-Methode und `boolean equals(Object)`, das von `Object` neu deklariert wird. Ist es ein gültiges `@FunctionalInterface`?
Was this page helpful?