1. Vertiefung in die funktionale Schnittstelle
Eine funktionale Schnittstelle ist eine Schnittstelle, die GENAU eine abstrakte (also nicht implementierte) Methode hat. Genau dadurch versteht Java: „Aha, hier kann man einen Lambda-Ausdruck einsetzen!“ oder einen Methodenverweis.
Damit niemand sich vertut, steht in solchen Schnittstellen normalerweise die Annotation @FunctionalInterface. Sie ist nicht verpflichtend, aber wenn Sie sie hinzufügen und versehentlich eine zweite abstrakte Methode schreiben, beschwert sich der Compiler sofort.
Beispiel:
@FunctionalInterface
interface MyAction {
void run();
}
MyAction action = () -> System.out.println("Hallo aus der Lambda!");
action.run(); // Gibt aus: Hallo aus der Lambda!
Wozu das Ganze?
- Ermöglicht die Verwendung von Lambda-Ausdrücken und Methodenreferenzen anstelle anonymer Klassen (weniger Boilerplate!).
- Zeigt dem Compiler, dass die Schnittstelle für funktionale Programmierung gedacht ist.
Fun Fact: In der Java-Standardbibliothek gibt es bereits Dutzende solcher Schnittstellen – das Rad muss nicht neu erfunden werden!
2. Überblick über Standard-funktionale Schnittstellen
Im Paket java.util.function „leben“ Dutzende funktionaler Schnittstellen. Schauen wir uns die vier beliebtesten an (sie haben die höchste „Besucherzahl“ unter allen Java-Schnittstellen).
| Schnittstelle | Eingabe | Rückgabe | Typische Verwendung |
|---|---|---|---|
|
|
|
Bedingung prüfen (Filtern) |
|
|
|
Eine Aktion an einem Objekt ausführen |
|
nichts | |
Ein Objekt liefern/erzeugen |
|
|
|
T in R transformieren |
Predicate<T>
Beschreibung: Eine Funktion, die ein Objekt vom Typ T annimmt und true oder false zurückgibt. Typisches Beispiel: Listen filtern. Schlüssel-Methode – test.
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Java")); // false
System.out.println(isLong.test("Functional")); // true
Consumer<T>
Beschreibung: Nimmt ein Objekt vom Typ T entgegen, führt eine Aktion aus und gibt nichts zurück. Schlüssel-Methode – accept.
Consumer<String> printer = s -> System.out.println("Drucke: " + s);
printer.accept("Hello, world!"); // Drucke: Hello, world!
Supplier<T>
Beschreibung: Nimmt nichts entgegen, gibt ein Objekt vom Typ T zurück. Man kann ihn sich als „Generator“ von Werten vorstellen. Schlüssel-Methode – get.
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // Zum Beispiel: 0.1234567
Function<T, R>
Beschreibung: Nimmt ein Objekt vom Typ T entgegen und gibt ein Objekt vom Typ R zurück. Typisches Beispiel: Daten transformieren. Schlüssel-Methode – apply.
Function<String, Integer> stringToLength = s -> s.length();
System.out.println(stringToLength.apply("Java")); // 4
Kurz: UnaryOperator, BinaryOperator, BiFunction
- UnaryOperator<T> – entspricht Function<T, T>: nimmt und gibt denselben Typ zurück.
- BinaryOperator<T> – entspricht BiFunction<T, T, T>: nimmt zwei T, gibt ein T zurück.
- BiFunction<T, U, R> – nimmt zwei verschiedene Typen und gibt einen dritten zurück.
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
3. Anwendungsbeispiele
Schauen wir, wie diese Schnittstellen in realen Aufgaben vorkommen – insbesondere in Collections und dem Stream-API.
Übergabe an Methoden von Collections und dem Stream-API
Beispiel 1: Predicate und Filtern
List<String> words = List.of("java", "stream", "lambda", "code");
List<String> longWords = words.stream()
.filter(word -> word.length() > 4) // Predicate<String>
.toList();
System.out.println(longWords); // [stream, lambda]
Beispiel 2: Consumer und forEach
words.forEach(word -> System.out.println("Wort: " + word)); // Consumer<String>
Beispiel 3: Function und map
List<Integer> lengths = words.stream()
.map(word -> word.length()) // Function<String, Integer>
.toList();
System.out.println(lengths); // [4, 6, 6, 4]
Beispiel 4: Supplier und das Generieren von Werten
Supplier<String> greetingSupplier = () -> "Hallo, Java!";
System.out.println(greetingSupplier.get()); // Hallo, Java!
Vergleich mit anonymen Klassen
Früher musste man so schreiben:
Predicate<String> isShort = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() < 5;
}
};
Mit Lambdas ist es um ein Vielfaches angenehmer:
Predicate<String> isShort = s -> s.length() < 5;
4. Praxis: Lambda-Ausdrücke für jede Schnittstelle schreiben
Wir implementieren eine kleine Anwendung – eine Benutzerliste. Jeder Benutzer wird durch die Klasse User dargestellt:
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Erstellen wir eine Liste von Benutzern:
List<User> users = List.of(
new User("Anna", 23),
new User("Boris", 17),
new User("Victoria", 31),
new User("George", 15)
);
Predicate: Erwachsene filtern
Predicate<User> isAdult = user -> user.getAge() >= 18;
List<User> adults = users.stream()
.filter(isAdult)
.toList();
System.out.println("Erwachsene: " + adults); // Erwachsene: [Anna (23), Victoria (31)]
Consumer: Benutzer ausgeben
Consumer<User> printUser = user -> System.out.println("Benutzer: " + user);
adults.forEach(printUser);
Supplier: Benutzer generieren
Supplier<User> randomUserSupplier = () -> {
String[] names = {"Danny", "Kate", "Alex"};
int randomAge = 10 + (int)(Math.random() * 30);
String randomName = names[(int)(Math.random() * names.length)];
return new User(randomName, randomAge);
};
User randomUser = randomUserSupplier.get();
System.out.println("Zufälliger Benutzer: " + randomUser);
Function: den Namen des Benutzers erhalten
Function<User, String> getName = user -> user.getName();
List<String> names = users.stream()
.map(getName)
.toList();
System.out.println("Namen: " + names); // Namen: [Anna, Boris, Victoria, George]
5. Nützliche Feinheiten
Verwendung im Stream-API: filter, map, forEach u. a.
Fassen wir alles zusammen und schreiben eine Transformationskette:
users.stream()
.filter(user -> user.getAge() >= 18) // Predicate<User>
.map(user -> user.getName().toUpperCase()) // Function<User, String>
.forEach(name -> System.out.println("Erwachsener: " + name)); // Consumer<String>
Ergebnis:
Erwachsener: ANNA
Erwachsener: VIKA
Spickzettel: was wo eingesetzt wird
| Wo verwendet | Welches Interface wird benötigt | Verwendungsbeispiel |
|---|---|---|
| filter (Stream) | |
|
| map (Stream) | |
|
| forEach (Stream, List) | |
|
| generate (Stream) | |
|
Warum ist es wichtig, funktionale Schnittstellen zu kennen?
- Sie bilden die Grundlage aller Lambda-Ausdrücke in Java.
- Sie ermöglichen universellen, wiederverwendbaren und prägnanten Code.
- Sie vereinfachen die Arbeit mit Collections, Streams und asynchronen Aufgaben.
Wann welche Schnittstelle verwenden?
- Predicate – wenn eine Bedingung geprüft werden soll (filtern, suchen).
- Consumer – wenn mit einem Objekt etwas getan werden soll (ausgeben, schreiben, senden).
- Supplier – wenn ein Objekt bezogen oder generiert werden soll (Fabriken, Generatoren).
- Function – wenn ein Objekt von einem Typ in einen anderen transformiert werden soll.
6. Typische Fehler im Umgang mit funktionalen Schnittstellen
Fehler Nr. 1: Falsche Wahl der Schnittstelle. Manchmal verwechseln Einsteiger Predicate und Function – zum Beispiel wird versucht, ein boolean aus Function zurückzugeben, statt aus Predicate. Merksatz: Predicate gibt immer boolean zurück, Function – jeden anderen Typ.
Fehler Nr. 2: Standard-Schnittstellen nicht verwenden. Oft werden eigene Schnittstellen wie „Checker“ mit der Methode boolean check(T t) geschrieben, statt Predicate zu verwenden. Besser die Standardtypen nutzen – sie werden überall unterstützt und machen den Code für andere Entwickler verständlicher.
Fehler Nr. 3: Lambda ist zu komplex. Wenn ein Lambda-Ausdruck zu einem Mini-Roman mit 10 Zeilen wird, sollte er in eine eigene Methode oder Klasse ausgelagert werden. Lambda steht für Kürze und Lesbarkeit.
Fehler Nr. 4: Vergessene Annotation @FunctionalInterface. Wenn Sie Ihre eigene funktionale Schnittstelle schreiben – vergessen Sie die Annotation nicht. Sie schützt vor zufälligen Fehlern (wie dem Hinzufügen einer zweiten abstrakten Methode).
Fehler Nr. 5: Veränderbaren Zustand innerhalb eines Lambdas verwenden. Wenn ein Lambda externe Variablen oder Collections verändert, kann das zu unerwarteten Bugs führen, besonders bei der Arbeit mit Threads. Nebenwirkungen besser vermeiden.
GO TO FULL VERSION