Wenn man frisch mit modernerem Java unterwegs ist, hat man oft zwei Begriffe im Kopf, die irgendwie zusammengehören, aber trotzdem gerne durcheinander geraten: Lambdas und Streams. Beides wirkt auf den ersten Blick "neu und modern", beide tauchen gerne zusammen im Code auf - und zack, im Kopf verschwimmt das. In diesem Artikel trennen wir das sauber: Was ist ein Lambda, was ist ein Stream, warum ist das nicht das Gleiche und wie spielen die beiden trotzdem zusammen.
Über beide Themen habe ich bereits separater Beiträge veröffentlich: Lambdas in Java und Streams in Java.
Was ist ein Lambda in Java überhaupt?
Ein Lambda-Ausdruck ist in Java im Kern nur eine kürzere Schreibweise für eine anonyme Implementierung eines Functional Interface. Heisst: Du schreibst eine kleine Funktion ohne Namen, die genau eine abstrakte Methode repräsentiert.
Ein ganz einfacher Lambda-Ausdruck sieht so aus:
Predicate<String> istLang = s -> s.length() > 5;
Das Ding speichert eine Funktion in der Variablen istLang, die einen String nimmt und ein boolean zurückgibt. Mehr ist es nicht. Kein Stream, keine Magie, keine Parallelisierung, nur eine Funktion als Wert.
Das könntest du auch "klassisch" so schreiben:
Predicate<String> istLang = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() > 5;
}
};
Das Lambda ist also einfach nur syntaktischer Zucker für eine Funktion, die einem Functional Interface entspricht. Wenn du dir das einmal klar machst, wird viel Verwirrung kleiner: Ein Lambda startet keinen Stream, verarbeitet keine Listen, es beschreibt nur was passieren soll, nicht worauf und nicht wie das ausgeführt wird.
Was ist ein Stream?
Ein Stream ist etwas ganz anderes: eine Abstraktion über eine Folge von Daten, die du in einer Pipeline von Operationen verarbeitest. Ein Stream ist kein Datenspeicher, keine Liste und auch keine spezielle Schleife, sondern eher eine "Verarbeitungsschiene" für Elemente.
Typisch sieht das so aus:
List<String> namen = List.of("Anna", "Bernd", "Chris", "Daniel");
List<String> gefiltert = namen.stream()
.filter(n -> n.length() > 4)
.map(String::toUpperCase)
.toList();
Hier passiert Folgendes:
namenist die eigentliche Datenquelle (eine Liste).namen.stream()erzeugt einen Stream über diese Liste.filterundmapsind Operationen auf diesem Stream.toList()ist eine terminale Operation, die die Pipeline wirklich ausführt und ein Ergebnis liefert.
Ein Stream ist also das "Wie und Worauf" der Verarbeitung. Er sagt: "Wir nehmen diese Datenquelle, schieben die Elemente durch mehrere Schritte und bekommen am Ende ein Ergebnis". Ohne ein Lambda könnte er das trotzdem tun - nur weniger bequem. Ohne Stream hat ein Lambda überhaupt keine Daten, an denen es in diesem Beispiel arbeiten könnte.
Lambdas vs. Streams: zwei verschiedene Ebenen
Der wichtigste Punkt: Lambdas und Streams sind verschiedene Ebenen in deinem Code.
- Ein Lambda ist eine Funktion (ein Stück Verhalten).
- Ein Stream ist eine Datenverarbeitungspipeline (eine Abfolge von Schritten über Elemente).
Das ist ein bisschen wie beim Kochen: Das Rezept (Ablauf der Schritte) ist der Stream, die einzelnen Zubereitungsschritte wie "Gemüse schneiden" oder "abschmecken" sind die Lambdas. Ohne Rezept kannst du einzelne Schritte haben, die aber nirgendwo hinführen. Ohne Schritte hast du ein Rezept, das nur aus Überschriften besteht.
In Java ist es konkret so:
- Streams definieren Methoden wie
filter,map,sortedusw. - Diese Methoden brauchen als Parameter Funktionen, z. B. ein
Predicatefürfilteroder eineFunctionfürmap. - Lambdas sind die bequeme Art, genau diese Funktionen zu übergeben.
Darum sieht Stream-Code fast immer "voll mit Lambdas" aus - aber das ist nur, weil Streams gerne Funktionen als Parameter brauchen. Es ist keine feste Kopplung: Du kannst Streams ohne Lambdas verwenden und Lambdas ohne Streams.
Streams ohne Lambdas
Um zu sehen, dass Streams und Lambdas getrennt sind, hilft ein Beispiel ohne Lambdas. Du kannst bei Stream-Operationen statt eines Lambdas auch eine benannte Methode mit Method Reference übergeben oder eine eigene Klasse bauen.
class IstLangGenug implements Predicate<String> {
@Override
public boolean test(String s) {
return s.length() > 4;
}
}
List<String> gefiltert = namen.stream()
.filter(new IstLangGenug())
.toList();
Hier ist kein Lambda im Spiel. Der Stream funktioniert trotzdem genau so wie vorher. Die Pipeline ist dieselbe, nur die Art, wie du die Funktion definierst, ist anders.
Noch kürzer mit Method Reference:
boolean istLangGenug(String s) {
return s.length() > 4;
}
List<String> gefiltert = namen.stream()
.filter(MeineKlasse::istLangGenug)
.toList();
Auch das kommt ohne Lambda-Ausdruck aus. Die Methodenreferenz ist technisch eng verwandt, aber syntaktisch etwas anderes. Der Punkt bleibt: Der Stream ist die Pipeline. Wie du die Funktionen bereitstellst (Lambda, anonyme Klasse, eigene Klasse, Method Reference), ist erst der nächste Schritt.
Lambdas ohne Streams
Genauso kannst du Lambdas wunderbar ohne Streams einsetzen. Du brauchst nur ein Functional Interface. Klassiker: Listener, Callbacks oder konfigurierbares Verhalten.
Runnable task = () -> System.out.println("Task laeuft");
new Thread(task).start();
Hier gibt es keinen Stream weit und breit. Das Lambda beschreibt lediglich, was passieren soll, wenn der Thread läuft. Die JVM führt das dann an der entsprechenden Stelle aus.
Oder als Strategie:
UnaryOperator<Integer> verdoppeln = x -> x * 2;
UnaryOperator<Integer> quadrieren = x -> x * x;
int ergebnis = anwenden(5, verdoppeln);
int anwenden(int wert, UnaryOperator<Integer> op) {
return op.apply(wert);
}
Auch hier: kein Stream, nur Funktionen, die du herumreichst. Lambdas sind also ein Feature der Sprache (Syntax + Functional Interfaces), Streams sind eine Bibliothek (das java.util.stream-API), die dieses Feature intensiv nutzt.
Warum die Verwechslung so häufig ist
Die Verwirrung kommt in der Praxis daher, dass du Streams fast immer mit Lambdas siehst. In vielen Beispielen heisst es direkt: "Wir filtern mit einem Lambda", "wir mappen mit einem Lambda" usw. Dazu kommen Einführungsartikel, in denen quasi alles gebündelt wird: "Seit Java 8 gibt es Lambdas, Streams und Functional Interfaces". Dann verschwimmt das schnell zu einem Paket.
Sauber getrennt:
- Functional Interfaces: definieren die Form der Funktionen (z. B.
Predicate<T>,Function<T,R>,Consumer<T>). - Lambdas: kurze Schreibweise, um eine Implementierung dieser Interfaces direkt hinzuschreiben.
- Streams: API, um Datenquellen mit Hilfe solcher Funktionen in Pipelines zu verarbeiten.
Wenn du dir diese Ebenen bewusst machst, kannst du Code besser lesen: "Ah, hier ist ein Stream, der arbeitet mit diesen Functions und Predicates, die über Lambdas beschrieben sind." Statt: "Irgendwas mit Lambdas, also wahrscheinlich ein Stream."
Wie Lambdas und Streams konkret zusammenspielen
Schauen wir uns eine kleine Pipeline genauer an und zerlegen, was genau wo passiert:
List<String> namen = List.of("Anna", "Bernd", "Chris", "Daniel");
List<String> ergebnis = namen.stream()
.filter(n -> n.startsWith("D"))
.map(String::toUpperCase)
.sorted()
.toList();
Was steckt da drin?
namen.stream(): Aus der Liste wird ein Stream. Hier ist noch kein Element verarbeitet, das ist nur die Startkonfiguration.filter(...): Erwartet einPredicate<String>. Das Lambdan -> n.startsWith("D")ist genau so ein Predicate.map(...): Erwartet eineFunction<String, String>. Die Method ReferenceString::toUpperCaseist so eine Function.sorted(): Nutzt entweder die natürliche Ordnung oder optional einComparator<String>, das könnte auch wieder ein Lambda sein.toList(): Terminale Operation, startet die Ausführung, sammelt das Ergebnis in einer neuen Liste.
Lambdas beschreiben hier einzelne Verarbeitungsschritte. Der Stream organisiert, dass jedes Element nacheinander (oder parallel, wenn du parallel() verwendest) durch diese Schritte durchläuft.
Typische Missverständnisse und Fallen
Ein paar Dinge, die gerade am Anfang gerne durcheinander geraten:
-
"Ein Lambda ist kürzer, also automatisch besser."
Nein. Lambdas sind praktisch, aber wenn die Logik länger oder komplexer wird, kann eine benannte Methode oder Klasse deutlich lesbarer sein. Streams mit drei verschachtelten Lambdas, die jeweils mehrere Zeilen haben, machen keinen glücklich. -
"Ich brauche Streams, um Lambdas zu verwenden."
Nein. Du kannst Lambdas überall einsetzen, wo ein Functional Interface erwartet wird: Collections-Methoden wieremoveIf, eigene APIs, Threading, Callbacks, was auch immer. -
"Ein Stream speichert meine Daten."
Nein. Der Stream hängt immer an einer Datenquelle (z. B. Liste, Array, Datei, Generator). Er ist eine Verarbeitungsschicht, kein Container. Wenn du aus einem Stream eine Liste machst, erzeugst du dabei wieder einen echten Container. -
"Streams sind automatisch schneller."
Streams und Lambdas sind nicht per se ein Performance-Upgrade gegenüber klassischen Schleifen. Manchmal ist es ähnlich schnell, manchmal langsamer, manchmal schneller. Es hängt von vielen Faktoren ab. Der Hauptvorteil ist erst einmal Lesbarkeit und Ausdruckskraft.
Ab wann ergibt der Einsatz von Streams und Lambdas Sinn?
Gerade am Anfang ist die Frage berechtigt: Muss ich das alles schon benutzen, oder reicht eine for-Schleife? Meine Meinung: Du musst nicht krampfhaft alles auf Streams drehen, aber du solltest früh verstehen, was hier passiert.
Ein Stream lohnt sich vor allem dann:
- wenn du mehrere Operationen hintereinander auf einer Sammlung von Daten ausführst (filtern, transformieren, sortieren, gruppieren)
- wenn die Reihenfolge der Schritte durch den Stream-Code besser lesbar wird als mehrere Schleifen oder if-Ketten
- wenn du später möglichst einfach parallelisieren willst (
parallelStream())
Lambdas lohnen sich immer dann, wenn du eine kleine Funktion "vor Ort" definieren willst, ohne dafür eine eigene Klasse oder Methode zu schreiben, und der Code trotzdem noch gut lesbar bleibt.
Fazit: klar trennen, bewusst kombinieren
Wenn du dir nur eine Sache aus diesem Artikel merkst, dann diese:
- Ein Lambda ist eine Art, eine Funktion kompakt aufzuschreiben.
- Ein Stream ist eine Pipeline, um Daten mit solchen Funktionen zu verarbeiten.
- Sie gehören zusammen, sind aber nicht dasselbe.
Sobald du das im Kopf getrennt hast, liest du modernen Java-Code entspannter: Du siehst den Datenfluss (Streams) und die einzelnen Verarbeitungsschritte (Lambdas bzw. Functions) als zwei Bereiche, die zusammenspielen. Und genau da wird es dann spannend - nicht, weil es "hip" ist, sondern weil dein Code damit oft klarer ausdrücken kann, was er eigentlich tut.
