CodeGym /Kurse /JAVA 25 SELF /Funktionale Schnittstellen: Predicate, Consumer, Supplier...

Funktionale Schnittstellen: Predicate, Consumer, Supplier, Function

JAVA 25 SELF
Level 49, Lektion 0
Verfügbar

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
Predicate<T>
T
boolean
Bedingung prüfen (Filtern)
Consumer<T>
T
void
Eine Aktion an einem Objekt ausführen
Supplier<T>
nichts
T
Ein Objekt liefern/erzeugen
Function<T, R>
T
R
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)
Predicate<T>
filter(u -> u.getAge() > 18)
map (Stream)
Function<T, R>
map(u -> u.getName())
forEach (Stream, List)
Consumer<T>
forEach(u -> System.out.println(u))
generate (Stream)
Supplier<T>
Stream.generate(() -> ...)

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.

1
Aufgabe
JAVA 25 SELF, Level 49, Lektion 0
Gesperrt
Analyse der Buchtitel in der Bibliothek der Fantastik 📚
Analyse der Buchtitel in der Bibliothek der Fantastik 📚
1
Aufgabe
JAVA 25 SELF, Level 49, Lektion 0
Gesperrt
Verwaltung der Spielmetriken im "Virtuelles Königreich" 🎮
Verwaltung der Spielmetriken im "Virtuelles Königreich" 🎮
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION