- 1 Section
- 10 Lessons
- unbegrenzt
- Java Fortgeschritten10
- 1.1OOP in Java: Klassen, Kapselung, Konstruktoren
- 1.2Vererbung in Java
- 1.3Polymorphismus und Methodenüberschreibung
- 1.4Interfaces und abstrakte Klassen
- 1.5Collections: ArrayList, HashMap, LinkedList
- 1.6Generics und Typsicherheit
- 1.7Streams und Lambda-Ausdrücke
- 1.8Dateioperationen: IO und NIO
- 1.9Unit-Tests in Java: JUnit 5
- 1.10Praxisprojekt: Anwendung mit OOP und Tests
Streams und Lambda-Ausdrücke
Mit Java 8 (2014) hat sich der typische Java-Code spürbar verändert. Zwei Features stehen im Zentrum: Lambda-Ausdrücke (kompakte Schreibweise für Funktionen) und Streams (eine Pipeline-API zur Verarbeitung von Daten). Sie gehören untrennbar zusammen – moderner Java-Code arbeitet ständig mit Lambdas in Streams.
Diese Lektion zeigt dir, wie du aus klassischen for-Schleifen elegante Streams machst, was eine Lambda genau ist, und welche Stream-Operationen (filter, map, reduce, collect) du im Alltag brauchst.
1) Was ist ein Lambda-Ausdruck?
Eine Lambda ist im Kern eine kurze, anonyme Funktion. Sie wird häufig anstelle einer ganzen Klasse mit einer einzigen Methode verwendet. Vergleich:
// Klassisch: anonyme innere Klasse Runnable r1 = new Runnable() { @Override public void run() { System.out.println("Hallo"); } }; // Modern: Lambda-Ausdruck Runnable r2 = () -> System.out.println("Hallo");
Beide tun dasselbe – das Lambda ist sieben Mal kürzer. Möglich wird das, weil Runnable ein Functional Interface ist: ein Interface mit genau einer abstrakten Methode. Der Compiler weiß, dass das Lambda diese eine Methode implementiert.
2) Lambda-Syntax
Eine Lambda hat drei Teile: Parameter links, ein Pfeil ->, dann der Rumpf rechts:
return – nötig bei mehreren Anweisungen.3) Functional Interfaces – die Empfänger von Lambdas
Eine Lambda kannst du überall einsetzen, wo ein Functional Interface erwartet wird. Java bietet eine ganze Bibliothek davon in java.util.function:
Beispiel:
Predicate<Integer> istGerade = n -> n % 2 == 0; Function<Integer, Integer> verdoppeln = x -> x * 2; Consumer<String> drucke = s -> System.out.println(s); System.out.println(istGerade.test(4)); // true System.out.println(verdoppeln.apply(3)); // 6 drucke.accept("Hi"); // Hi
4) Method References – noch kürzer
Wenn eine Lambda nur eine bestehende Methode aufruft, kannst du sie noch knapper schreiben – mit dem Methodenreferenz-Operator :::
// Lambda List<String> namen = List.of("Anna", "Ben"); namen.forEach(s -> System.out.println(s)); // Method Reference (identisch in Wirkung) namen.forEach(System.out::println);
Vier Arten von Methodenreferenzen:
- Statische Methode:
Integer::parseInt– ruftInteger.parseInt(s)auf - Methode eines Objekts:
System.out::println– ruftSystem.out.println(x)auf - Methode einer Klasse:
String::toUpperCase– ruftx.toUpperCase()auf - Konstruktor:
ArrayList::new– ruftnew ArrayList()auf
5) Stream – die Pipeline
Ein Stream ist eine Folge von Elementen, die durch eine Pipeline aus Operationen geschickt wird. Klassischer Aufbau: Quelle → Zwischenoperationen → Endoperation.
List.of(1, 2, 3, 4, 5, 6).stream() – Quelle, liefert die Elemente..filter(n -> n % 2 == 0) – behält nur gerade Zahlen..map(n -> n * n) – quadriert jedes Element..collect(Collectors.toList()) – Endoperation, sammelt zur Liste.collect, count) startet die Verarbeitung. Vorher passiert intern noch nichts; das nennt man lazy evaluation.6) Das gleiche als Code
Was du im Diagramm gesehen hast, in einer Zeile:
List<Integer> result = List.of(1, 2, 3, 4, 5, 6).stream() .filter(n -> n % 2 == 0) .map(n -> n * n) .collect(Collectors.toList()); // [4, 16, 36]
Vergleich mit klassischer Schleife:
List<Integer> result = new ArrayList<>(); for (int n : List.of(1, 2, 3, 4, 5, 6)) { if (n % 2 == 0) { result.add(n * n); } }
Stream-Variante: deklarativ – sie sagt was getan werden soll. Loop-Variante: imperativ – sie sagt wie. Bei einfachen Aufgaben ist die Loop oft noch klarer; bei komplexen Pipelines gewinnt der Stream deutlich.
7) Wichtige Zwischenoperationen
Zwischenoperationen verändern den Stream und geben einen neuen Stream zurück. Die wichtigsten:
filter(Predicate)– behält nur Elemente, die das Prädikat erfüllenmap(Function)– wandelt jedes Element in ein anderes umflatMap(Function)– wie map, aber „flacht" verschachtelte Streams abdistinct()– entfernt Duplikatesorted()odersorted(Comparator)– sortiertlimit(n)– nimmt nur die ersten n Elementeskip(n)– überspringt die ersten n Elementepeek(Consumer)– führt etwas für jedes Element aus, gibt Stream unverändert weiter (zum Debuggen)
8) Wichtige Endoperationen
Endoperationen schließen die Pipeline und liefern ein Endergebnis:
collect(Collector)– sammelt zu Collection, Map, String usw.toList()– seit Java 16 direkt verfügbar als.toList()forEach(Consumer)– führt für jedes Element etwas auscount()– zählt die ElementefindFirst()/findAny()– liefert das erste/irgendein Element alsOptionalanyMatch,allMatch,noneMatch– Boolean-Testsmin(Comparator)/max(Comparator)– Extremwertereduce(...)– fasst alle Elemente zu einem Wert zusammen
9) Collectors – das mächtige Werkzeug
Die Klasse java.util.stream.Collectors bietet vorgefertigte Sammler:
List<String> namen = List.of("Anna", "Ben", "Carla"); // In Liste sammeln List<String> gross = namen.stream() .map(String::toUpperCase) .collect(Collectors.toList()); // In Set sammeln (entfernt Duplikate) Set<String> einzigartig = namen.stream().collect(Collectors.toSet()); // Zu String zusammenfügen String joined = namen.stream().collect(Collectors.joining(", ")); // "Anna, Ben, Carla" // Gruppieren – sehr mächtig Map<Integer, List<String>> nachLaenge = namen.stream() .collect(Collectors.groupingBy(String::length)); // {3=[Ben], 4=[Anna], 5=[Carla]} // Zählen pro Gruppe Map<Integer, Long> anzahl = namen.stream() .collect(Collectors.groupingBy( String::length, Collectors.counting()));
groupingBy ersetzt im modernen Java oft GROUP BY aus SQL für In-Memory-Daten. Sehr leistungsfähig.
10) reduce – einen Wert herausfalten
reduce kombiniert alle Elemente eines Streams zu einem einzelnen Ergebnis. Klassischer Anwendungsfall: Summieren:
List<Integer> zahlen = List.of(1, 2, 3, 4); // reduce mit Startwert int summe = zahlen.stream() .reduce(0, (a, b) -> a + b); // 10 // reduce ohne Startwert – liefert Optional Optional<Integer> max = zahlen.stream() .reduce(Integer::max); max.ifPresent(System.out::println); // 4
Für die häufigsten Aggregationen gibt es Spezial-Methoden: mapToInt(...).sum(), mapToInt(...).average(), count(). Die sind lesbarer als reduce.
11) Streams sind nicht wiederverwendbar
Wichtiges Detail: ein Stream darf nur einmal benutzt werden. Sobald eine Endoperation gelaufen ist, ist der Stream verbraucht:
Stream<String> s = namen.stream(); s.count(); s.count(); // IllegalStateException
Wenn du dieselben Daten mehrfach durch Streams schicken willst, ruf .stream() jedes Mal neu auf der Collection auf.
12) Streams sammeln – modern
Seit Java 16 gibt es eine kürzere Variante als collect(Collectors.toList()):
List<String> result = namen.stream() .filter(n -> n.length() > 3) .toList(); // kompakt und unveränderlich
Beachte: das Ergebnis ist unveränderlich. Wer ändern will, packt es in new ArrayList<>(result).
13) Stream-Quellen
Streams kommen nicht nur aus Listen. Verschiedene Quellen:
// Aus Collection Stream<String> s1 = liste.stream(); // Aus Array Stream<String> s2 = Arrays.stream(new String[]{"a", "b"}); // Aus festen Werten Stream<String> s3 = Stream.of("a", "b", "c"); // Zahlenreihe IntStream s4 = IntStream.range(1, 10); // 1..9 // Aus Datei (siehe L8) Files.lines(Path.of("data.txt")) .filter(l -> !l.isBlank()) .forEach(System.out::println);
IntStream, LongStream, DoubleStream sind primitive Streams – effizienter als Stream<Integer>, weil sie ohne Boxing arbeiten.
14) Parallel Streams
Streams können parallel verarbeitet werden – mit nur einer Methodenänderung:
long count = grosseListe.parallelStream() .filter(x -> aufwendigerTest(x)) .count();
Das nutzt mehrere CPU-Kerne. Aber Vorsicht:
- Lohnt sich erst bei vielen Elementen und aufwendigen Operationen
- Lambda-Funktionen müssen thread-safe sein – keine geteilten veränderbaren Zustände
- Reihenfolge kann verloren gehen, wenn nicht explizit erhalten
- Overhead durch Aufteilung kann bei kleinen Daten schaden
Im Zweifel nicht parallel – sequenzielle Streams sind oft schon schnell genug.
15) Optional – sicheres Null-Handling
Viele Stream-Methoden geben Optional<T> zurück: ein Wrapper, der entweder einen Wert enthält oder leer ist:
Optional<String> gefunden = namen.stream() .filter(n -> n.startsWith("A")) .findFirst(); // Sicherer Umgang gefunden.ifPresent(System.out::println); String wert = gefunden.orElse("unbekannt"); // In Stream weiterverarbeiten gefunden.map(String::toUpperCase).ifPresent(System.out::println);
Optional macht null-checks explizit. Statt if (x != null) nutzt du x.ifPresent(...) oder x.orElse(...). Vermeidet NullPointerException.
Optional-Parametern sind unleserlich.
16) Häufige Fehler
- Stream nach Endoperation weiter benutzen: gibt IllegalStateException
- forEach für Seiteneffekte missbraucht: wenn du in einer parallelen Pipeline aus mehreren Threads in dieselbe Liste schreibst, gibt es Daten-Race-Probleme
- Stream auf null geprüft:
stream != null– das ist falsch. LieberOptional.ofNullable(...). - Lambda mit Seiteneffekten: Streams sollten möglichst seiteneffektfrei sein. Logik gehört in die Pipeline, nicht in zwischengelagerte Listen
- Parallel Stream voreilig: ohne Messung sehen Parallel Streams gut aus, sind aber oft langsamer als sequenziell
- map vs. flatMap verwechselt:
mapergibt Stream<Stream<T>> bei verschachtelten Daten –flatMapmacht daraus einen flachen Stream
Zusammenfassung
Lambda-Ausdrücke sind kompakte anonyme Funktionen mit Syntax (params) -> body; sie funktionieren mit jedem Functional Interface – einem Interface mit genau einer abstrakten Methode. Streams sind Pipelines aus Quelle, Zwischenoperationen (filter, map, sorted) und einer Endoperation (collect, forEach, count, reduce). Die Verarbeitung ist lazy: erst die Endoperation startet alles. Mit Collectors sammelst du Ergebnisse in Listen, Sets, Maps oder Strings; groupingBy ist das mächtigste Werkzeug für Gruppierungen. Methodenreferenzen (Class::method) kürzen einzeilige Lambdas weiter ab. Optional<T> macht „kann fehlen" explizit und ist ein typisches Stream-Rückgabe-Format. Streams sind einmalig verwendbar; parallel Streams nur bei großen Datenmengen mit thread-safen Operationen.
