Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting
Mit Einblicken von Nova Trent (Junior Dev) und Jamal Hassan (Backend Developer)
Schwierigkeit: 🟡 Mittel
Lesezeit: 35 Minuten
Voraussetzungen: Tag 4 (Lambda-Ausdrücke)
Kurs: Java Erweiterte Techniken – Tag 5 von 10
📖 Java Erweiterte Techniken – Alle Tage
📍 Du bist hier: Tag 5
⚡ Das Wichtigste in 30 Sekunden
Dein Problem: Du weißt, wie Lambdas aussehen – aber woher weiß Java, welcher „Typ“ ein Lambda ist?
Die Lösung: Functional Interfaces – Interfaces mit genau einer abstrakten Methode. Sie sind der „Typ“ für Lambdas.
Heute lernst du:
- ✅ Was Functional Interfaces sind und warum sie wichtig sind
- ✅ Die Standard-Interfaces: Function, Predicate, Consumer, Supplier
- ✅ Eigene Functional Interfaces mit
@FunctionalInterface - ✅ Komposition: Funktionen kombinieren
- ✅ Primitive Spezialisierungen für Performance
Für wen ist dieser Artikel?
- 🌱 Anfänger: Du verstehst endlich, was hinter
Function<T,R>steckt - 🌿 Erfahrene: Komposition und Chaining meistern
- 🌳 Profis: Eigene Interfaces designen, Performance optimieren
👋 Elyndra: „Das fehlende Puzzlestück“
Hi! 👋
Elyndra hier für Tag 5. Gestern haben wir Lambdas geschrieben – aber da war eine Frage offen:
// Das hier funktioniert: Comparator<String> comp = (a, b) -> a.compareTo(b); // Aber warum nicht das? var lambda = x -> x * 2; // COMPILE ERROR! Welcher Typ?
Das Problem: Ein Lambda ist nur eine Kurzschreibweise. Java muss wissen, welchen Typ das Lambda hat – und das erfährt es durch das Functional Interface.
Heute lernst du: Die wichtigsten Functional Interfaces kennen, eigene schreiben, und sie elegant kombinieren.
🖼️ Die Standard Functional Interfaces

Abbildung 1: Die vier wichtigsten Functional Interfaces
💡 Spoiler:
java.util.functionenthält 43 Interfaces, nicht nur 4! Die Basis-Vier sind der Einstieg – die vollständige Übersicht findest du im PROFESSIONALS-Bereich.
🟢 GRUNDLAGEN
Was ist ein Functional Interface?
Ein Functional Interface ist ein Interface mit genau einer abstrakten Methode (SAM – Single Abstract Method).
// Das ist ein Functional Interface:
@FunctionalInterface
public interface Rechner {
int berechne(int a, int b);
}
// Verwendung mit Lambda:
Rechner addition = (a, b) -> a + b;
Rechner multiplikation = (a, b) -> a * b;
System.out.println(addition.berechne(3, 4)); // 7
System.out.println(multiplikation.berechne(3, 4)); // 12
Die @FunctionalInterface Annotation:
- Ist optional, aber empfohlen
- Der Compiler prüft, dass wirklich nur eine abstrakte Methode existiert
- Dokumentiert die Absicht
@FunctionalInterface
public interface Broken {
void methode1();
void methode2(); // COMPILE ERROR! Zwei abstrakte Methoden!
}
💬 Nova fragt: „Was ist mit default und static Methoden?“
Gute Frage! Die zählen nicht als „abstrakte Methoden“:
@FunctionalInterface
public interface MitExtras {
void hauptMethode(); // Die eine abstrakte Methode
default void hilfsmethode() { // OK - default
System.out.println("Hilfe!");
}
static void utility() { // OK - static
System.out.println("Utility!");
}
}
🎯 Der Aha-Moment: Vom Interface zum Lambda

Abbildung 2: Der Weg vom Interface zum Lambda – von 10 Zeilen zu einer
Lass uns ein eigenes Functional Interface bauen und Schritt für Schritt sehen, warum Lambdas funktionieren:
Schritt 1: Das Interface definieren
@FunctionalInterface
public interface StringTransformer {
String transform(String input);
}
Das ist unser „Vertrag“: Nimm einen String, gib einen String zurück. Eine abstrakte Methode = Functional Interface ✓
Schritt 2: Klassische Implementierung (vor Java 8)
// Eigene Klasse
public class UpperCaseTransformer implements StringTransformer {
@Override
public String transform(String input) {
return input.toUpperCase();
}
}
// Verwendung
StringTransformer transformer = new UpperCaseTransformer();
System.out.println(transformer.transform("hallo")); // HALLO
Funktioniert – aber eine ganze Klasse für eine Zeile Logik? 🙄
Schritt 3: Anonyme Klasse (etwas besser)
StringTransformer transformer = new StringTransformer() {
@Override
public String transform(String input) {
return input.toUpperCase();
}
};
System.out.println(transformer.transform("hallo")); // HALLO
Keine separate Datei mehr – aber immer noch viel Boilerplate.
Schritt 4: Lambda (seit Java 8) 🎉
StringTransformer transformer = input -> input.toUpperCase();
System.out.println(transformer.transform("hallo")); // HALLO
Eine Zeile! Der Compiler weiß:
StringTransformerhat genau eine Methode:transform(String)- Also muss das Lambda diese Methode implementieren
inputist der Parameter,input.toUpperCase()ist der Rückgabewert
Schritt 5: Method Reference (noch kürzer)
StringTransformer transformer = String::toUpperCase;
System.out.println(transformer.transform("hallo")); // HALLO
Warum funktioniert das?
| Code | Compiler versteht |
|---|---|
String::toUpperCase | „Ruf toUpperCase() auf dem String auf“ |
input -> input.toUpperCase() | Gleiche Logik, nur expliziter |
| Anonyme Klasse | Gleiche Logik, mit Overhead |
💬 Jamal: „Der Schlüssel ist: Ein Functional Interface hat genau eine abstrakte Methode. Deshalb weiß der Compiler, was das Lambda implementieren soll. Kein Raten nötig!“
Praktisches Beispiel: Mehrere Transformer
@FunctionalInterface
public interface StringTransformer {
String transform(String input);
}
// Verschiedene Implementierungen als Lambdas
StringTransformer toUpper = s -> s.toUpperCase();
StringTransformer toLower = s -> s.toLowerCase();
StringTransformer reverse = s -> new StringBuilder(s).reverse().toString();
StringTransformer addPrefix = s -> ">>> " + s;
// Anwendung
String text = "Hallo Welt";
System.out.println(toUpper.transform(text)); // HALLO WELT
System.out.println(toLower.transform(text)); // hallo welt
System.out.println(reverse.transform(text)); // tleW ollaH
System.out.println(addPrefix.transform(text)); // >>> Hallo Welt
Das ist die Magie: Ein Interface, unendlich viele Verhaltensweisen – definiert in einer Zeile!
Die Vier Wichtigsten: Function, Predicate, Consumer, Supplier
Java stellt im Package java.util.function über 40 Functional Interfaces bereit. Die wichtigsten vier:
| Interface | Signatur | Beschreibung | Beispiel |
|---|---|---|---|
Function<T,R> | R apply(T t) | Transformation T→R | s -> s.length() |
Predicate<T> | boolean test(T t) | Bedingung prüfen | s -> s.isEmpty() |
Consumer<T> | void accept(T t) | Verarbeitung ohne Rückgabe | s -> System.out.println(s) |
Supplier<T> | T get() | Wert erzeugen | () -> Math.random() |
Merksatz:
- Function = Eingabe → Ausgabe (Transformation)
- Predicate = Eingabe → true/false (Filterung)
- Consumer = Eingabe → nichts (Seiteneffekt)
- Supplier = nichts → Ausgabe (Erzeugung)
Function<T,R> – Transformation
Nimmt einen Wert vom Typ T und gibt einen Wert vom Typ R zurück.
// String -> Integer
Function<String, Integer> laenge = s -> s.length();
System.out.println(laenge.apply("Hallo")); // 5
// Integer -> String
Function<Integer, String> zuText = n -> "Zahl: " + n;
System.out.println(zuText.apply(42)); // "Zahl: 42"
// Person -> String (Getter als Function)
Function<Person, String> getName = Person::getName;
Verketten mit andThen und compose:
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
// trim zuerst, dann upper
Function<String, String> combined = trim.andThen(upper);
System.out.println(combined.apply(" hallo ")); // "HALLO"
// upper zuerst, dann trim
Function<String, String> reversed = trim.compose(upper);
System.out.println(reversed.apply(" hallo ")); // "HALLO" (gleich hier)
Predicate<T> – Bedingung prüfen
Nimmt einen Wert und gibt true oder false zurück.
Predicate<String> istLeer = s -> s.isEmpty();
Predicate<String> istLang = s -> s.length() > 10;
Predicate<Integer> istGerade = n -> n % 2 == 0;
System.out.println(istLeer.test("")); // true
System.out.println(istLang.test("Hi")); // false
System.out.println(istGerade.test(4)); // true
Kombinieren mit and, or, negate:
Predicate<String> nichtLeer = s -> !s.isEmpty();
Predicate<String> kurzGenug = s -> s.length() <= 10;
// Kombinieren
Predicate<String> gueltig = nichtLeer.and(kurzGenug);
System.out.println(gueltig.test("")); // false (leer)
System.out.println(gueltig.test("Hi")); // true
System.out.println(gueltig.test("Das ist zu lang!")); // false
// Oder-Verknüpfung
Predicate<String> aOderB = s -> s.startsWith("A");
Predicate<String> startsMitAoderB = aOderB.or(s -> s.startsWith("B"));
// Negieren
Predicate<String> nichtMitA = aOderB.negate();
Consumer<T> – Verarbeitung
Nimmt einen Wert, gibt nichts zurück. Typisch für Ausgaben oder Seiteneffekte.
Consumer<String> drucker = s -> System.out.println(s);
Consumer<String> logger = s -> System.err.println("[LOG] " + s);
drucker.accept("Hallo"); // Gibt "Hallo" aus
// Verketten mit andThen
Consumer<String> beides = drucker.andThen(logger);
beides.accept("Test");
// Gibt aus: Test
// Gibt aus: [LOG] Test
Praktisch mit forEach:
List<String> namen = List.of("Max", "Anna", "Tom");
namen.forEach(System.out::println);
namen.forEach(name -> System.out.println("Hallo " + name));
Supplier<T> – Erzeugung
Nimmt nichts, gibt einen Wert zurück. Ideal für Lazy Evaluation.
Supplier<Double> zufall = () -> Math.random(); Supplier<LocalDateTime> jetzt = LocalDateTime::now; Supplier<List<String>> leereListe = ArrayList::new; System.out.println(zufall.get()); // 0.123456... System.out.println(jetzt.get()); // 2025-01-15T14:30:00
Lazy Evaluation:
// Teuer zu berechnen
Supplier<String> teureBerechnung = () -> {
// Simuliere lange Berechnung
try { Thread.sleep(1000); } catch (Exception e) {}
return "Ergebnis";
};
// Wird nur aufgerufen wenn nötig!
Optional<String> optional = Optional.empty();
String wert = optional.orElseGet(teureBerechnung); // Nur wenn empty!
🟡 PROFESSIONALS
BiFunction, BiPredicate, BiConsumer
Für zwei Eingabeparameter:
// BiFunction<T, U, R>: (T, U) -> R
BiFunction<String, String, String> concat = (a, b) -> a + b;
System.out.println(concat.apply("Hallo", " Welt")); // "Hallo Welt"
// BiPredicate<T, U>: (T, U) -> boolean
BiPredicate<String, Integer> laengeOk = (s, max) -> s.length() <= max;
System.out.println(laengeOk.test("Hallo", 10)); // true
// BiConsumer<T, U>: (T, U) -> void
BiConsumer<String, Integer> ausgabe = (name, alter) ->
System.out.println(name + " ist " + alter + " Jahre alt");
ausgabe.accept("Max", 25);
UnaryOperator und BinaryOperator
Spezialisierungen wenn Eingabe und Ausgabe den gleichen Typ haben:
// UnaryOperator<T> extends Function<T, T>
UnaryOperator<String> upper = String::toUpperCase;
UnaryOperator<Integer> verdoppeln = n -> n * 2;
// BinaryOperator<T> extends BiFunction<T, T, T>
BinaryOperator<Integer> addieren = (a, b) -> a + b;
BinaryOperator<String> laengster = (a, b) -> a.length() > b.length() ? a : b;
System.out.println(addieren.apply(3, 4)); // 7
System.out.println(laengster.apply("Hi", "Hallo")); // "Hallo"
Primitive Spezialisierungen
Für Performance gibt es Varianten für int, long, double:
// Kein Autoboxing nötig! IntPredicate istGerade = n -> n % 2 == 0; IntFunction<String> zuText = n -> "Zahl: " + n; IntSupplier zufallsInt = () -> (int)(Math.random() * 100); IntConsumer drucker = n -> System.out.println(n); IntUnaryOperator verdoppeln = n -> n * 2; IntBinaryOperator addieren = (a, b) -> a + b; // Spezielle Konverter IntToDoubleFunction zuDouble = n -> n * 1.0; ToIntFunction<String> laenge = String::length;
Warum primitive Varianten?
// Mit Boxing (langsamer, mehr Speicher) Function<Integer, Integer> f1 = n -> n * 2; // Ohne Boxing (schneller, weniger Speicher) IntUnaryOperator f2 = n -> n * 2;
Bei großen Datenmengen macht das einen echten Unterschied!
📋 Alle 43 Interfaces im Überblick

Abbildung 3: Alle 43 Functional Interfaces in java.util.function
Die komplette Liste:
| Kategorie | Interfaces | Signatur |
|---|---|---|
| Basis | ||
Function<T,R> | T → R | |
Predicate<T> | T → boolean | |
Consumer<T> | T → void | |
Supplier<T> | () → T | |
| Bi-Varianten | ||
BiFunction<T,U,R> | (T, U) → R | |
BiPredicate<T,U> | (T, U) → boolean | |
BiConsumer<T,U> | (T, U) → void | |
| Operatoren | ||
UnaryOperator<T> | T → T | |
BinaryOperator<T> | (T, T) → T | |
| int-Familie | ||
IntFunction<R> | int → R | |
IntPredicate | int → boolean | |
IntConsumer | int → void | |
IntSupplier | () → int | |
IntUnaryOperator | int → int | |
IntBinaryOperator | (int, int) → int | |
| long-Familie | ||
LongFunction<R> | long → R | |
LongPredicate | long → boolean | |
LongConsumer | long → void | |
LongSupplier | () → long | |
LongUnaryOperator | long → long | |
LongBinaryOperator | (long, long) → long | |
| double-Familie | ||
DoubleFunction<R> | double → R | |
DoublePredicate | double → boolean | |
DoubleConsumer | double → void | |
DoubleSupplier | () → double | |
DoubleUnaryOperator | double → double | |
DoubleBinaryOperator | (double, double) → double | |
| Konverter (To…) | ||
ToIntFunction<T> | T → int | |
ToLongFunction<T> | T → long | |
ToDoubleFunction<T> | T → double | |
ToIntBiFunction<T,U> | (T, U) → int | |
ToLongBiFunction<T,U> | (T, U) → long | |
ToDoubleBiFunction<T,U> | (T, U) → double | |
| Konverter (Between Primitives) | ||
IntToLongFunction | int → long | |
IntToDoubleFunction | int → double | |
LongToIntFunction | long → int | |
LongToDoubleFunction | long → double | |
DoubleToIntFunction | double → int | |
DoubleToLongFunction | double → long | |
| ObjX-Consumer | ||
ObjIntConsumer<T> | (T, int) → void | |
ObjLongConsumer<T> | (T, long) → void | |
ObjDoubleConsumer<T> | (T, double) → void | |
| Boolean | ||
BooleanSupplier | () → boolean |
💡 Warum so viele? Primitive Spezialisierungen vermeiden Autoboxing. Bei Millionen von Operationen spart das erheblich Speicher und CPU-Zeit!
Eigene Functional Interfaces
Manchmal braucht man eigene:
// Für checked Exceptions
@FunctionalInterface
public interface ThrowingSupplier<T> {
T get() throws Exception;
}
// Mit drei Parametern
@FunctionalInterface
public interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
}
// Verwendung
TriFunction<String, String, String, String> join =
(a, b, c) -> a + ", " + b + ", " + c;
System.out.println(join.apply("Eins", "Zwei", "Drei"));
🔵 BONUS
Funktionskomposition in der Praxis
// Pipeline für Textverarbeitung
Function<String, String> pipeline =
((Function<String, String>) String::trim)
.andThen(String::toLowerCase)
.andThen(s -> s.replace(" ", "_"));
System.out.println(pipeline.apply(" Hello World ")); // "hello_world"
Methoden höherer Ordnung
Funktionen, die Funktionen zurückgeben:
// Funktion die einen Multiplikator zurückgibt
Function<Integer, Function<Integer, Integer>> multiplikator =
faktor -> zahl -> zahl * faktor;
Function<Integer, Integer> mal3 = multiplikator.apply(3);
Function<Integer, Integer> mal5 = multiplikator.apply(5);
System.out.println(mal3.apply(10)); // 30
System.out.println(mal5.apply(10)); // 50
💬 Real Talk: Wann welches Interface?
Java Fleet Büro, Freitag 11:00. Tom schaut verwirrt auf seinen Code.
Tom: „Elyndra, ich hab hier eine Methode und weiß nicht, welches Functional Interface ich nehmen soll…“
// Was soll das sein?
??? processor = (String s) -> {
System.out.println("Processing: " + s);
return s.toUpperCase();
};
Elyndra: „Okay, analysieren wir: Hat es einen Eingabeparameter?“
Tom: „Ja, einen String.“
Elyndra: „Gibt es einen Rückgabewert?“
Tom: „Ja, auch einen String.“
Elyndra: „Dann ist es eine Function<String, String>. Oder noch präziser: UnaryOperator<String>, weil Ein- und Ausgabe den gleichen Typ haben.“
Nova: kommt dazu „Ich hab mir eine Eselsbrücke gebaut:“
Eingabe? → Nein? → Supplier<T>
→ Ja? → Rückgabe? → Nein? → Consumer<T>
→ boolean? → Predicate<T>
→ anderer Typ? → Function<T,R>
Tom: „Ah, das hilft! Und was ist mit meinem System.out.println drin?“
Jamal: „Seiteneffekte in einer Function sind… okay, aber nicht ideal. Wenn du nur ausgeben willst, nimm Consumer<T>. Wenn du transformieren willst, nimm Function<T,R> und lass die Ausgabe weg.“
❓ FAQ
Frage 1: Warum gibt es so viele Interfaces?
Wegen Type Erasure! Function<Integer, Integer> und Function<String, String> sind zur Laufzeit beide nur Function. Die verschiedenen Interfaces ermöglichen unterschiedliche Signaturen im gleichen Scope.
Plus: Primitive Spezialisierungen vermeiden Autoboxing.
Frage 2: Kann ich Lambdas als Parameter übergeben?
Ja! Das ist der Hauptanwendungsfall:
public static <T> List<T> filter(List<T> liste, Predicate<T> bedingung) {
List<T> ergebnis = new ArrayList<>();
for (T element : liste) {
if (bedingung.test(element)) {
ergebnis.add(element);
}
}
return ergebnis;
}
// Verwendung
List<String> namen = List.of("Max", "Anna", "Tom", "Alexandra");
List<String> mitA = filter(namen, s -> s.startsWith("A"));
// ["Anna", "Alexandra"]
Frage 3: Was ist Function.identity()?
Eine Funktion, die ihr Argument unverändert zurückgibt:
Function<String, String> identity = Function.identity();
// Gleich wie: s -> s
// Nützlich bei Streams:
Map<String, String> map = list.stream()
.collect(Collectors.toMap(Function.identity(), String::toUpperCase));
Frage 4: Bernd sagt, Functional Interfaces seien „nur fancy Interfaces“?
seufz Technisch stimmt das. Aber das unterschlägt den Paradigmenwechsel:
- Vor Java 8: Interfaces = Verträge für Klassen
- Seit Java 8: Functional Interfaces = Typen für Verhalten
Du kannst jetzt Verhalten als Parameter übergeben, in Variablen speichern, und komponieren. Das ist funktionale Programmierung in Java!
🔍 „behind the code“ oder „in my feels“? Manche Team-Mitglieder haben persönliche Logs…
🎁 Cheat Sheet
🟢 Die Basis-Vier
Function<T, R> // T → R apply(T t) Predicate<T> // T → boolean test(T t) Consumer<T> // T → void accept(T t) Supplier<T> // → T get()
🟡 Bi-Varianten & Operatoren
BiFunction<T, U, R> // (T, U) → R BiPredicate<T, U> // (T, U) → boolean BiConsumer<T, U> // (T, U) → void UnaryOperator<T> // T → T (extends Function) BinaryOperator<T> // (T, T) → T (extends BiFunction)
🔵 Komposition
// Function f.andThen(g) // f zuerst, dann g f.compose(g) // g zuerst, dann f // Predicate p.and(q) // p UND q p.or(q) // p ODER q p.negate() // NICHT p // Consumer c.andThen(d) // c zuerst, dann d
🔴 Primitive Spezialisierungen (Performance!)
// int (analog für long, double) IntFunction<R> // int → R IntPredicate // int → boolean IntConsumer // int → void IntSupplier // () → int IntUnaryOperator // int → int IntBinaryOperator // (int, int) → int // Konverter ToIntFunction<T> // T → int IntToLongFunction // int → long ObjIntConsumer<T> // (T, int) → void
🎨 Challenge für dich!
🟢 Level 1 – Einsteiger
- [ ] Erstelle Predicate, Function, Consumer, Supplier für Strings
- [ ] Kombiniere zwei Predicates mit
and()undor() - [ ] Verkette zwei Functions mit
andThen()
Geschätzte Zeit: 15-30 Minuten
🟡 Level 2 – Fortgeschritten
- [ ] Schreibe eine
filter()-Methode mit Predicate-Parameter - [ ] Implementiere eine Textverarbeitungs-Pipeline
- [ ] Nutze BiFunction für einen Taschenrechner
Geschätzte Zeit: 30-45 Minuten
🔵 Level 3 – Profi
- [ ] Erstelle ein eigenes
@FunctionalInterfacemit Exception - [ ] Implementiere Currying:
Function<A, Function<B, C>> - [ ] Optimiere Code mit primitiven Spezialisierungen
Geschätzte Zeit: 45-90 Minuten
📦 Downloads
| Projekt | Für wen? | Download |
|---|---|---|
| tag05-functional-starter.zip | 🟢 Mit TODOs | ⬇️ Download |
| tag05-functional-complete.zip | 🟡 Musterlösung | ⬇️ Download |
🔗 Weiterführende Links
🇩🇪 Deutsch
| Ressource | Beschreibung |
|---|---|
| Rheinwerk: Functional Interfaces | Detailliertes Kapitel |
🇬🇧 Englisch
| Ressource | Beschreibung | Level |
|---|---|---|
| Oracle: java.util.function | Alle Interfaces | 🟢 |
| Baeldung: Functional Interfaces | Praxisbeispiele | 🟡 |
👋 Geschafft! 🎉
Was du heute gelernt hast:
✅ Functional Interfaces = Interfaces mit einer abstrakten Methode
✅ Die vier Basis-Interfaces: Function, Predicate, Consumer, Supplier
✅ Bi-Varianten und Operatoren für speziellere Fälle
✅ Alle 43 Interfaces in java.util.function (Primitive, Konverter, etc.)
✅ Komposition mit andThen, compose, and, or
✅ Primitive Spezialisierungen für Performance (kein Autoboxing!)
Fragen? elyndra.valen@java-developer.online
📖 Weiter geht’s!
← Vorheriger Tag: Tag 4: Lambda-Ausdrücke
→ Nächster Tag: Tag 6: Stream-API
Tags: #Java #FunctionalInterface #Function #Predicate #Consumer #Supplier #Tutorial
📚 Das könnte dich auch interessieren
© 2025 Java Fleet Systems Consulting | java-developer.online

