Alle Tabs der Lerneinheit (Erklärung · Interaktiv verstehen · Praxis-Übung · Klausur-Quiz) als durchgehender Text. Ideal zum Wiederholen vor der Klausur, und für Suchmaschinen wie Google, Bing und KI-Suche (ChatGPT, Perplexity).
Erklärung
Stell dir vor du willst eine Liste sortieren, aber nicht alphabetisch, sondern nach Notenschnitt, oder nur die ungeraden Zahlen behalten, oder jede Zahl verdoppeln. Du müsstest jedes Mal eine neue Funktion schreiben. Lambdas sind Mini-Funktionen, die du wie Werte weitergeben kannst, und so beliebige Logik in fertige Methoden einbauen. In Java wird ein Lambda dabei immer als Instanz eines funktionalen Interfaces verwendet, zum Beispiel Predicate für eine Filter-Bedingung oder Function für eine Transformation: der Zieltyp links bestimmt, welche Methode das Lambda implementiert. Praktisch zahlt sich das in Klausuren dreifach aus: du kannst Stream-Ausgaben Schritt für Schritt bestimmen, eine Lambda-Syntax als korrekt oder fehlerhaft erkennen und ein Lambda dem passenden funktionalen Interface zuordnen.
Seit Java 8 (2014) Teil der Sprache, heute also Standard und kein Spezialwissen mehr. Klausur-Vorkommen in rund 5 von 13 gesichteten WInf-Prog-1-Klausuren (eigene, anonymisierte Klausur-Sammlung deutschsprachiger Hochschulen, Zeitraum ca. 2020 bis 2025; P3, Vertiefung, in Prog-2 deutlich häufiger).
Die Idee in einem Satz
Lambda: In Java eine anonyme Funktionsbeschreibung, die über ein funktionales Interface typisiert wird, und die du wie einen Wert behandeln kannst, speichern, weitergeben, später ausführen. Higher-Order-Funktion: Eine Funktion, die andere Funktionen als Parameter nimmt oder zurückgibt.
Vor Java 8: anonyme Klassen
Die klassische Lösung war eine anonyme Klasse:
List<String> namen = Arrays.asList("Charlie", "Alice", "Bob");
Collections.sort(namen, new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
5 Zeilen für "sortiere nach Länge". Mit Lambda:
Collections.sort(namen, (a, b) -> a.length() - b.length());
Von 5 Zeilen auf 1 Zeile, gleicher Effekt. Das ist der Hauptvorteil von Lambdas: Prägnanz ohne Funktionsverlust.
Lambda-Syntax
// Klassisch (3 Varianten):
(int a, int b) -> { return a + b; } // mit Typ-Annotation + Block
(a, b) -> { return a + b; } // Typen inferiert
(a, b) -> a + b // Single-Expression, return implizit
Faustregeln:
- 1 Parameter? Klammern weglassen:
x -> x * 2 - 1 Expression? Klammern + return weg:
(a, b) -> a + b - Mehrere Statements? Block + return nötig:
(a, b) -> { int s = a + b; return s; }
Funktionale Interfaces
Ein Lambda implementiert immer ein funktionales Interface, ein Interface mit genau einer abstrakten Methode (default- und static-Methoden im Interface zählen dabei nicht mit, sie haben ja schon einen Rumpf). Das ist ein typischer Klausur-Fallstrick: ein Interface mit einer abstrakten plus drei default-Methoden ist immer noch funktional.
Die wichtigsten für den Einstieg sind Function, Predicate, Consumer und Supplier, alle aus dem Paket java.util.function. Diese vier Karten oben musst du in der Klausur abrufbar haben, sie decken Transformation, Test, Aktion und Erzeugung ab. Daneben gibt es Spezialvarianten, die du nur bei Bedarf nachschlägst: BiFunction für zwei Eingaben, UnaryOperator als Function mit gleichem Ein-/Ausgabetyp, primitive Varianten wie IntPredicate.
| Interface | Signatur | Eingang → Ausgang | Wofür |
|---|---|---|---|
Function<T, R> | R apply(T t) | T → R | Transformation (z. B. map) |
Predicate<T> | boolean test(T t) | T → boolean | Filter-Bedingung (z. B. filter) |
Consumer<T> | void accept(T t) | T → (nichts) | Aktion mit Seiteneffekt (z. B. forEach) |
Supplier<T> | T get() | (nichts) → T | Erzeuger ohne Eingabe |
BiFunction<T,U,R> | R apply(T t, U u) | T, U → R | zwei Eingaben → ein Ergebnis |
Diese Tabelle ist deine Signatur-Karte: links das Interface, in der Mitte was rein- und rausgeht, rechts die passende Stream-Operation. Lies eine Zeile immer als Pfeil, dann ordnest du jedes Lambda blitzschnell zu.
Beispiele:
Function<Integer, Integer> verdoppeln = x -> x * 2;
verdoppeln.apply(5); // 10
Predicate<Integer> isGerade = n -> n % 2 == 0;
isGerade.test(4); // true
Consumer<String> drucker = s -> System.out.println(s);
drucker.accept("Hallo"); // gibt Hallo aus
Higher-Order-Methoden: map, filter, forEach
Listen / Streams haben Methoden, die andere Funktionen als Argument nehmen. Höhere Ordnung = sie verarbeiten Funktionen. Stell dir die Kette als Fließband vor, jede Stufe bekommt eine Liste rein und gibt eine Liste (oder einen Wert) raus:
[1,2,3,4,5,6]
|
v filter (gerade/ungerade) -> wirft Elemente raus
[2,4,6]
|
v map (x*2 / x*x) -> verändert jedes Element
[4,16,36]
|
v reduce (Summe / Produkt) -> kombiniert alle zu einem Wert
56
Genau diese drei Stufen schaltest du im Visualizer weiter unten per Checkbox dazu oder weg und siehst die Zwischen-Liste nach jeder Stufe.
List<Integer> zahlen = List.of(1, 2, 3, 4, 5);
// filter: behalte nur die geraden
zahlen.stream()
.filter(n -> n % 2 == 0) // [2, 4]
.forEach(System.out::println);
// map: verdoppele jede Zahl
List<Integer> verdoppelt = zahlen.stream()
.map(n -> n * 2) // [2, 4, 6, 8, 10]
.collect(Collectors.toList());
// reduce: alle aufsummieren
int summe = zahlen.stream()
.reduce(0, (acc, n) -> acc + n); // 15
Faustregel:
- filter, wirft Elemente raus
- map, verändert jedes Element
- reduce, kombiniert alle zu einem
- forEach, macht etwas mit jedem (Seiteneffekt)
Eine kombinierte Pipeline löst du am sichersten, indem du nach jeder Operation die Zwischen-Liste hinschreibst. So liest sich Stream.of(1,2,3,4,5,6).filter(n -> n % 2 == 0).map(n -> n * n).reduce(0, Integer::sum) von links nach rechts:
Start: [1, 2, 3, 4, 5, 6]
filter (n gerade) -> [2, 4, 6]
map (n * n) -> [4, 16, 36]
reduce (0, +) -> 4 + 16 + 36 = 56
In echt werden Streams lazy ausgewertet: Java baut keine echten Zwischen-Listen, sondern schiebt jedes Element einzeln durch die ganze Kette. Die Zwischen-Listen oben sind nur deine Denkstütze für die Klausur, das Ergebnis ist exakt dasselbe.
Reihenfolge zählt: filter VOR map ist effizienter, weil map dann weniger Elemente transformieren muss. Vertauscht (erst map, dann filter) kommt hier zwar dasselbe Ergebnis raus, aber das ist nicht garantiert, sobald filter auf das Original-Element prüft.
Method-References: noch kürzer
Manchmal ist das Lambda nur ein Aufruf einer existierenden Methode. Dann gibt's eine Kurzschreibweise mit :::
// Lambda
list.forEach(s -> System.out.println(s));
// Method-Reference (identisch)
list.forEach(System.out::println);
// String::length statt s -> s.length()
list.stream().map(String::length);
Lesbarer in vielen Fällen, aber nicht überall sinnvoll, bei nicht-trivialen Lambdas lieber explizit.
Functional Composition
Funktionen lassen sich verketten:
Function<Integer, Integer> verdoppeln = x -> x * 2;
Function<Integer, Integer> plusZehn = x -> x + 10;
Function<Integer, Integer> kombi = verdoppeln.andThen(plusZehn);
kombi.apply(3); // (3 * 2) + 10 = 16
Function<Integer, Integer> kombi2 = verdoppeln.compose(plusZehn);
kombi2.apply(3); // (3 + 10) * 2 = 26
andThen: erst die eine, dann die andere. compose: umgekehrt (innere zuerst).
Klausur-Faustregeln
1. Lambda implementiert ein funktionales Interface. Genau EINE abstrakte Methode. Die wichtigsten für den Einstieg sind Function, Predicate, Consumer und Supplier aus java.util.function; weitere Spezialvarianten gibt es, brauchst du aber selten auswendig.
2. Lambda-Syntax kennen. Single-Param: keine Klammern (x -> x*2). Single-Expression: kein return, kein Block ((a,b) -> a+b).
3. Higher-Order-Methoden machen mehr mit weniger Code. map/filter/reduce sind funktionale Standard-Operationen.
4. Method-Reference :: ist Syntax-Sugar für ein Lambda das eine bestehende Methode aufruft. System.out::println statt s -> System.out.println(s).
5. Lambdas sind nicht Threads. Ein Lambda läuft NICHT automatisch parallel, es ist nur eine Funktion. Für Parallelität: stream().parallel() oder ExecutorService.
Häufige Stolpersteine
1. return vergessen bei mehrzeiligem Body. (a, b) -> { a + b } ist Compile-Fehler, entweder (a, b) -> a + b oder (a, b) -> { return a + b; }.
2. Effectively-final-Regel. Lambda kann nur lokale Variablen lesen, die effectively final sind (nicht verändert nach der Zuweisung). Innerhalb des Lambdas reassign → Compile-Error.
int x = 10;
Function<Integer, Integer> f = n -> n + x; // ok
x = 20; // ← x ist nicht mehr effectively final → Compile-Error oben
3. Stream nicht 2x nutzen. Ein Stream ist EINMAL nutzbar. stream.filter(...).count() und dann nochmal stream.forEach(...) → IllegalStateException.
4. null in Streams. null kann durch filter/map laufen und Probleme später machen. Vorher .filter(Objects::nonNull) als Schutz.
5. Lambda mit Seiteneffekt in map/filter. map und filter sollten pur sein, also nebenwirkungsfrei (= keinen externen State ändern, nur aus den Eingaben ein Ergebnis zurückgeben). Wenn du Seiteneffekte willst → forEach am Ende.
Klausur-Beispielaufgabe
Beispiel (typischer Klausur-Stil, zur Übung konstruiert).
Gegeben sei folgender Code:
List<Integer> werte = List.of(1, 2, 3, 4, 5, 6);
int ergebnis = werte.stream()
.filter(n -> n % 2 == 1)
.map(n -> n * n)
.reduce(0, Integer::sum);
(a) Welchen Wert hat ergebnis? Gib die Zwischen-Liste nach jeder Operation an. (b) Welches funktionale Interface erwartet filter, welches map? (c) Das folgende Lambda kompiliert nicht, korrigiere es: (a, b) -> { a + b }. (d) Schreibe werte.forEach(x -> System.out.println(x)) mit einer Method-Reference.
Lösung:
- (a) Schrittweise (siehe Abschnitt Higher-Order-Methoden):
filter(ungerade) →[1, 3, 5],map(quadrieren) →[1, 9, 25],reducemit Summe →1 + 9 + 25 = 35. Alsoergebnis = 35. - (b)
filtererwartet einPredicate<Integer>(T → boolean),mapeineFunction<Integer, Integer>(T → R). Beides steht in der Signatur-Karte im Abschnitt Funktionale Interfaces. - (c) Der Block-Body
{ ... }braucht einreturn(siehe Lambda-Syntax und Stolperstein 1). Korrekt:(a, b) -> a + b(Single-Expression, return implizit) oder(a, b) -> { return a + b; }. - (d)
werte.forEach(System.out::println)(siehe Method-References). Funktional identisch zum Lambda.
Interaktiv verstehen
Higher-Order-Pipeline
Der Visualizer startet mit der festen Liste [1, 2, 3, 4, 5, 6]. Du aktivierst oder deaktivierst per Checkbox drei Operationen und siehst nach jeder Stufe die Zwischen-Liste und am Ende das Endergebnis:
- filter, wahlweise gerade oder ungerade behalten
- map, wahlweise verdoppeln (
n * 2) oder quadrieren (n * n) - reduce, wahlweise Summe oder Produkt
Schalte Operationen einzeln dazu und beobachte, wie sich die Liste nach jeder Stufe verändert. So entwickelst du Intuition für filter, map, reduce und ihre Zusammenarbeit, genau das brauchst du für Stream-Auswertungs-Aufgaben.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
Klausur-Tipp: Bei "Was gibt der folgende Stream zurück?", geh Operation für Operation durch. filter wirft raus, map verändert, reduce kombiniert. Notiere dir nach jeder Operation die Zwischen-Liste aufs Papier, nie nur im Kopf zusammenfassen, das ist die häufigste Fehlerquelle in Stream-Aufgaben.
Code-Stepper: Schritt für Schritt
Drei Demos zum Durchklicken. Klick Step oder Auto und beobachte, wie sich Variablen, Zwischen-Listen und Konsole pro Schritt verändern. Die Annotation unten am Code erklärt, was in diesem Step passiert.
1. Stream-Kette: filter, map, reduce auswerten
Die Klausur-Standardaufgabe: eine Stream-Kette von Hand auswerten. Hier laufen [1..6] durch filter (gerade), map (quadrieren) und reduce (Summe). Achte darauf, wie die Liste nach jeder Operation schrumpft oder sich verwandelt.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
2. Funktionales Interface: Predicate per Lambda
Ein Lambda ist nichts Magisches, es ist eine Instanz eines funktionalen Interfaces. Hier wird ein Predicate<Integer> durch ein Lambda gebaut und dann zweimal mit test(...) aufgerufen. Die abstrakte Methode test wird direkt durch den Lambda-Rumpf implementiert.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
3. Effectively-final: warum das Reassign bricht
Ein Lambda darf lokale Variablen nur lesen, wenn sie effectively final sind. Sobald die Variable nach der Zuweisung noch verändert wird, ist sie es nicht mehr, und die Zeile mit dem Lambda wird zum Compile-Error. Hier siehst du, an welcher Stelle der Compiler abbricht.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
Klausur-Faustregeln (Quick-Check)
Drei Faustregeln, die in Stream- und Lambda-Aufgaben fast immer Punkte bringen:
- Stream-Auswertung schrittweise: Bei jeder Stream-Kette jede Operation einzeln durchgehen (
filter→map→reduce) und die Zwischen-Liste notieren. Nie im Kopf zusammenfassen, das ist die häufigste Fehlerquelle. - Effectively-final-Regel: Ein Lambda kann lokale Variablen nur lesen, wenn sie nach der Zuweisung nicht mehr verändert werden. Ein Reassign der Variable macht die Lambda-Zeile zum Compile-Error.
- Method-Reference
::ist Syntax-Zucker:System.out::printlnunds -> System.out.println(s)sind funktional identisch und in der Klausur austauschbar. Lass dich von::nicht verwirren, übersetze es im Kopf ins gleichwertige Lambda.
Praxis-Übung
Lambdas & Higher-Order, Praxis-Übung
6 Aufgaben zu Lambda-Syntax, funktionalen Interfaces, Stream-Operationen.
Klausurfragen mit Lösungen (6)
- F1.Welche Lambda-Syntax ist KORREKT für 'verdoppele x'?
Antwort: x -> x * 2
Erklärung: Java-Syntax ist `x -> x * 2`. JavaScript wäre `=>`, Python `lambda x:`, klassisches Java `function(x)`. Pfeil-Operator `->` ist Java-Spezifika.
- F2.Was ist ein 'funktionales Interface'?
Antwort: Ein Interface mit genau einer abstrakten Methode
Erklärung: Funktionales Interface = genau EINE abstrakte Methode (default-Methoden zählen nicht). Beispiele: Function, Predicate, Consumer, Comparator. Nur funktionale Interfaces können von Lambdas implementiert werden.
- F3.Ein Lambda läuft automatisch in einem eigenen Thread (parallel).
Antwort: Falsch
Erklärung: FALSCH. Lambdas sind reine Funktionen, kein Threading. Für Parallelität: `stream().parallel()` oder ExecutorService. Lambdas sind nur eine kompakte Schreibweise, Ausführung ist sequenziell wie normaler Code.
Typ: Wahr/Falsch
- F4.Welche Higher-Order-Methode WIRFT Elemente RAUS, die ein Kriterium nicht erfüllen?
Antwort: filter
Erklärung: filter behält nur die Elemente, für die der Predicate true ist. map TRANSFORMIERT jedes (kein Element verschwindet), reduce KOMBINIERT zu einem Wert, forEach iteriert.
- F5.Welche Aussagen über Lambdas sind RICHTIG?
Richtige Antworten: Lambdas können effectively-final lokale Variablen lesen; `x -> x * 2` ist valides Java; Method-References wie `String::length` sind eine Kurzschreibweise; Ein funktionales Interface hat 1 abstrakte Methode
Erklärung: Richtig: effectively-final Zugriff erlaubt, Lambda-Syntax mit ->, Method-References, 1 abstrakte Methode. Falsch: Lambdas können lokale Variablen NICHT reassign (Compile-Error); Streams sind EINMAL nutzbar (IllegalStateException sonst).
Typ: Multi-Select
- F6.Ordne das funktionale Interface seiner Signatur zu:
Zuordnungen:
- Function<T, R> → T → R (Transformation)
- Predicate<T> → T → boolean (Filter-Bedingung)
- Consumer<T> → T → void (Action mit Seiteneffekt)
- Supplier<T> → () → T (Erzeuger ohne Input)
Erklärung: Die 4 Basis-Interfaces aus java.util.function. Function (transformiert), Predicate (testet), Consumer (verbraucht), Supplier (erzeugt).
Typ: Zuordnung
Klausur-Quiz
Klausurfragen mit Lösungen (6)
- F1.Was gibt aus: Stream.of(1,2,3,4).filter(n -> n%2==0).map(n -> n*10).reduce(0, Integer::sum)?
Antwort: 60
Erklärung: Schritt 1 filter: [2,4]. Schritt 2 map *10: [20, 40]. Schritt 3 reduce mit sum: 20+40=60. Klausur-Klassiker: jeden Schritt einzeln durchgehen.
- F2.Welche Lambda-Syntax ist KORREKT für 2 Parameter?
Antwort: (a, b) -> a + b
Erklärung: Bei MEHREREN Parametern sind Klammern PFLICHT: `(a, b) -> a + b`. Bei einem Parameter sind Klammern optional: `x -> x*2` oder `(x) -> x*2`. `=>` ist JavaScript, nicht Java.
- F3.System.out::println und s -> System.out.println(s) sind funktional identisch.
Antwort: Wahr
Erklärung: RICHTIG. Beides ist ein Consumer<String> mit gleicher Wirkung. `::` ist Method-Reference-Syntax, syntaktischer Zucker für das entsprechende Lambda.
Typ: Wahr/Falsch
- F4.Im Code: int x = 5; List<Integer> l = List.of(1,2,3); l.stream().forEach(n -> x = x + n);
Antwort: Compile-Fehler: x ist nicht effectively final
Erklärung: Compile-Fehler! Lambdas dürfen lokale Variablen nur LESEN, wenn sie effectively final sind. `x = x + n` ist eine Reassignment → Bricht das. Lösung: int[]{5} (Array bleibt final, Inhalt wandelt) oder besser: reduce verwenden.
- F5.Ein Lambda implementiert ein {{1}} Interface mit genau einer abstrakten Methode. filter braucht ein {{2}}, map braucht eine {{3}}. Die Stream-Methode, die alle Elemente zu einem Wert kombiniert, heißt {{4}}.
Lösungen pro Lücke:
- {{1}}: funktionales / functional
- {{2}}: Predicate
- {{3}}: Function
- {{4}}: reduce
Erklärung: Standard-Vokabular Lambdas + Streams. Funktionales Interface, Predicate (filter), Function (map), reduce (Aggregation). In einer deutschsprachigen Klausur ist "funktionales" die bevorzugte Schreibweise; das englische "functional" akzeptieren wir hier nur als Synonym.
Typ: Lückentext
- F6.Welche Stream-Pipeline gibt eine Liste der quadrierten geraden Zahlen aus [1..10] zurück?
Richtige Reihenfolge:
- Stream.of(1,2,3,4,5,6,7,8,9,10)
- .filter(n -> n % 2 == 0)
- .map(n -> n * n)
- .collect(Collectors.toList())
Erklärung: Erst Quelle, dann filter (gerade), dann map (quadrieren), dann sammeln. Reihenfolge wichtig: filter VOR map ist effizienter (weniger Elemente zu transformieren). Endresultat: [4, 16, 36, 64, 100].
Typ: Reihenfolge