Java Streams & Lambdas

6 Kernkonzepte
Die wichtigsten Konzepte der funktionalen Programmierung in Java: Lambda · Funktionale Interfaces · Stream API · Intermediate/ Terminal Operations · Collectors · Parallele Streams Mit Java 8 wurden Lambdas und die Stream-API eingeführt – sie ermöglichen funktionale, deklarative Datenverarbeitung und machen Code lesbarer und prägnanter.

Lambda-Ausdrücke – Funktionen als Parameter

(parameter) -> { ausdruck; }
(a, b) -> a + b (name) -> { System.out.println(name); } () -> 42 // Keine Parameter

Lambda-Ausdrücke sind anonyme Funktionen, die als Parameter übergeben werden können. Sie sind die Grundlage für funktionale Programmierung in Java.

Syntax-Formen

Form Beschreibung Beispiel
(param) -> ausdruck Einzelner Ausdruck (impliziter return) x -> x * 2
(param) -> { ... } Block mit mehreren Anweisungen name -> { System.out.println(name); }
(p1, p2) -> ausdruck Mehrere Parameter (a, b) -> a + b
() -> ausdruck Keine Parameter () -> Math.random()
param -> ausdruck Klammern optional bei einem Parameter x -> x * x
Beispiele
// Vergleich: anonyme Klasse vs. Lambda
// Vor Java 8 (anonyme Klasse)
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hallo");
}
};
// Mit Lambda
Runnable r2 = () -> System.out.println("Hallo");
// Comparator mit Lambda
Comparator<String> comp = (s1, s2) -> s1.compareTo(s2);
// List sortieren mit Lambda
list.sort((a, b) -> a.length() - b.length());
Tipp: Bei einem einzigen Parameter können die Klammern weggelassen werden: x -> x * 2. Bei mehreren Parametern oder ohne Parameter sind Klammern erforderlich.

Funktionale Interfaces – @FunctionalInterface

Predicate · Function · Consumer · Supplier
Predicate<String> isLong = s -> s.length() > 5; Function<String, Integer> len = s -> s.length(); Consumer<String> print = s -> System.out.println(s);

Funktionale Interfaces haben genau eine abstrakte Methode. Sie sind die Grundlage für Lambdas und werden mit @FunctionalInterface annotiert.

Die wichtigsten funktionalen Interfaces

Interface Methode Beschreibung
Predicate<T> boolean test(T t) Prüft eine Bedingung (true/false)
Function<T,R> R apply(T t) Transformiert T → R
Consumer<T> void accept(T t) Verarbeitet T (kein Rückgabewert)
Supplier<T> T get() Liefert ein T (kein Parameter)
BiFunction<T,U,R> R apply(T t, U u) Transformiert T,U → R
BiPredicate<T,U> boolean test(T t, U u) Prüft Bedingung mit 2 Parametern
BiConsumer<T,U> void accept(T t, U u) Verarbeitet T und U
UnaryOperator<T> T apply(T t) Function T → T (gleicher Typ)
BinaryOperator<T> T apply(T t1, T t2) BiFunction T,T → T
Beispiele
// Predicate – Bedingung prüfen
Predicate<Integer> isEven = n -> n % 2 == 0;
boolean result = isEven.test(4); // true
// Function – transformieren
Function<String, Integer> length = s -> s.length();
int len = length.apply("Hallo"); // 5
// Consumer – verarbeiten
Consumer<String> print = s -> System.out.println(s);
print.accept("Hello"); // Ausgabe: Hello
// Supplier – liefert Werte
Supplier<Double> random = () -> Math.random();
double r = random.get(); // Zufallszahl
Tipp: Eigene funktionale Interfaces können mit @FunctionalInterface definiert werden – der Compiler prüft dann, dass es nur eine abstrakte Methode gibt.

Stream API – Datenströme verarbeiten

stream() · parallelStream()
list.stream() .filter(s -> s.length() > 3) .map(String::toUpperCase) .forEach(System.out::println);

Streams sind Sequenzen von Elementen, die mit funktionalen Operationen verarbeitet werden. Sie sind lazy (werden erst bei terminaler Operation ausgeführt) und nicht veränderbar (die Quelle bleibt unverändert).

Erstellen von Streams

Quelle Code
Liste / Collection list.stream()
Array Arrays.stream(array)
Werte (varargs) Stream.of("a", "b", "c")
Unendlicher Stream Stream.iterate(0, n -> n + 1)
Zufallszahlen new Random().ints(10)
Datei-Zeilen Files.lines(path)
Beispiele
// Stream aus Liste
List<String> namen = Arrays.asList("Anna", "Ben", "Clara");
namen.stream().forEach(System.out::println);
// Stream aus Array
int[] zahlen = {1, 2, 3, 4, 5};
Arrays.stream(zahlen).filter(n -> n % 2 == 0).forEach(System.out::println);
// Stream.of()
Stream.of("a", "b", "c").map(String::toUpperCase).forEach(System.out::println);
Tipp: Ein Stream kann nur einmal konsumiert werden! Nach einer terminalen Operation ist der Stream geschlossen und kann nicht erneut verwendet werden.

Operationen – Intermediate & Terminal

filter · map · sorted · forEach · collect · reduce
// Intermediate (lazy) filter(predicate) · map(function) · sorted() · distinct() · limit(n) · skip(n) // Terminal (trigger Ausführung) forEach(consumer) · collect(collector) · reduce(binaryOp) · count() · anyMatch() · findFirst()

Intermediate-Operationen sind lazy und geben einen neuen Stream zurück. Terminal-Operationen starten die Verarbeitung und liefern ein Ergebnis oder einen Seiteneffekt.

Die wichtigsten Operationen

Typ Operation Beschreibung
Intermediate filter(Predicate) Filtert Elemente
Intermediate map(Function) Transformiert jedes Element
Intermediate flatMap(Function) Transformiert zu Stream und flacht ab
Intermediate sorted() / sorted(Comparator) Sortiert die Elemente
Intermediate distinct() Entfernt Duplikate
Intermediate limit(n) Begrenzt auf n Elemente
Intermediate skip(n) Überspringt n Elemente
Intermediate peek(Consumer) Führt Aktion für jedes Element aus (Debugging)
Terminal forEach(Consumer) Führt Aktion für jedes Element aus
Terminal collect(Collector) Sammelt Elemente in einer Collection
Terminal reduce(BinaryOperator) Reduziert auf einen einzelnen Wert
Terminal count() Zählt Elemente
Terminal anyMatch(Predicate) Prüft, ob mindestens eines zutrifft
Terminal allMatch(Predicate) Prüft, ob alle zutreffen
Terminal findFirst() Gibt erstes Element zurück
Beispiele
List<Integer> zahlen = Arrays.asList(1, 2, 3, 4, 5, 6);
// filter + map + collect
List<Integer> geradeQuadrate = zahlen.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.collect(Collectors.toList());
// reduce – Summe
int summe = zahlen.stream().reduce(0, (a, b) -> a + b);
// anyMatch – prüfen
boolean hatGrosse = zahlen.stream().anyMatch(n -> n > 10);
// sorted + limit
List<String> topDrei = namen.stream()
.sorted()
.limit(3)
.collect(Collectors.toList());
Tipp: Ein Stream besteht aus einer Quelle, beliebig vielen Intermediate-Operationen und einer Terminal-Operation. Ohne Terminal-Operation wird nichts ausgeführt!

Collectors – Ergebnisse sammeln

toList · toSet · toMap · joining · groupingBy
list.stream().collect(Collectors.toList()); list.stream().collect(Collectors.joining(", ")); list.stream().collect(Collectors.groupingBy(s -> s.length()));

Collectors sammeln die Elemente eines Streams in verschiedenen Datenstrukturen – Listen, Sets, Maps oder Strings.

Die wichtigsten Collectors

Collector Beschreibung Beispiel
toList() Sammelt in eine List .collect(Collectors.toList())
toSet() Sammelt in ein Set (Duplikate entfernt) .collect(Collectors.toSet())
toMap(keyMapper, valueMapper) Sammelt in eine Map .collect(Collectors.toMap(s -> s, s -> s.length()))
joining(delimiter) Fügt Strings mit Trennzeichen zusammen .collect(Collectors.joining(", "))
groupingBy(classifier) Gruppiert nach einem Schlüssel .collect(Collectors.groupingBy(s -> s.length()))
counting() Zählt Elemente einer Gruppe .collect(Collectors.groupingBy(s -> s.length(), counting()))
summingInt() Summiert int-Werte .collect(Collectors.summingInt(s -> s.length()))
averagingInt() Durchschnitt von int-Werten .collect(Collectors.averagingInt(s -> s.length()))
Beispiele
List<String> namen = Arrays.asList("Anna", "Ben", "Clara");
// toList
List<String> liste = namen.stream().collect(Collectors.toList());
// toMap – Name → Länge
Map<String, Integer> map = namen.stream()
.collect(Collectors.toMap(s -> s, s -> s.length()));
// joining – Komma-getrennt
String joined = namen.stream().collect(Collectors.joining(", "));
// groupingBy – nach Länge gruppieren
Map<Integer, List<String>> gruppen = namen.stream()
.collect(Collectors.groupingBy(s -> s.length()));
// groupingBy + counting – Anzahl pro Länge
Map<Integer, Long> counts = namen.stream()
.collect(Collectors.groupingBy(s -> s.length(), Collectors.counting()));
Tipp: Collectors.groupingBy() ist extrem mächtig – es kann mit nachgeschalteten Collectors kombiniert werden, um komplexe Gruppierungen und Aggregationen durchzuführen.

Parallele Streams – Multi-Core verarbeiten

parallelStream() · parallel()
list.parallelStream() .filter(s -> s.length() > 3) .map(String::toUpperCase) .forEach(System.out::println);

Parallele Streams nutzen den Fork/Join-Pool zur parallelen Verarbeitung von Daten. Sie können die Leistung bei großen Datenmengen deutlich verbessern.

Wichtige Hinweise

Aspekt Beschreibung
Erstellung collection.parallelStream() oder stream().parallel()
Thread-Pool ForkJoinPool.commonPool() (kann angepasst werden)
Thread-sicher Operationen müssen threadsicher oder unabhängig sein
Ordnung forEach() ist nicht geordnet, forEachOrdered() bewahrt Ordnung
Overhead Nur sinnvoll bei großen Datenmengen oder CPU-intensiven Operationen
Beispiele
// Paralleler Stream
List<Integer> zahlen = IntStream.range(1, 1_000_000).boxed().collect(Collectors.toList());
// Parallel summiert (schneller bei großen Listen)
long summe = zahlen.parallelStream().mapToLong(n -> n).sum();
// Parallele Verarbeitung mit forEachOrdered (Ordnung bewahren)
zahlen.parallelStream().filter(n -> n % 2 == 0)
.limit(100)
.forEachOrdered(System.out::println);
// Seriell vs. Parallel – Entscheidungshilfe
// Kleinere Daten: seriell (kein Overhead)
// Große Daten + CPU-intensiv: parallel
// Unabhängige Operationen: parallel
⚠️ Achtung: Parallele Streams sind nicht immer schneller – der Overhead kann bei kleinen Datenmengen überwiegen. Operationen mit Seiteneffekten (z.B. forEach mit nicht-threadsicherem Code) können zu Fehlern führen!

Java Streams & Lambdas im Überblick

λ Lambda-Ausdruck
(x) -> x * 2
@FunctionalInterface Funktionale Interfaces
Predicate, Function, Consumer
Stream Datenstrom
.stream() .parallelStream()
Intermediate Lazy-Operationen
filter, map, sorted
Terminal Auslösende Operation
collect, reduce, forEach
Collector Ergebnis sammeln
toList, groupingBy

Quick Summary

->
Lambda
Predicate
Funktionales Interface
stream()
Stream erstellen
filter()
Intermediate
collect()
Terminal
parallel
Parallele Verarbeitung
list.stream().filter(x -> x > 5).map(x -> x * 2).collect(Collectors.toList())