1. Einen Stream erstellen
Um das Stream API zu verwenden, muss man zuerst einen Stream aus einer Collection oder einem Array erzeugen.
Beispiele zur Erstellung eines Streams
// Aus einer Liste
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();
// Aus einem Array
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
// Aus einzelnen Werten
Stream<String> letters = Stream.of("A", "B", "C");
Kurz:
- list.stream() — für Collections
- Arrays.stream(array) — für Arrays
- Stream.of(...) — für einzelne Werte
Beispiel im Kontext unserer Anwendung
Nehmen wir an, wir haben eine Liste von Benutzern:
List<String> users = List.of("John", "Anna", "Peter", "Alex");
Stream<String> userStream = users.stream();
Zwischen- und Terminaloperationen
Wichtig: Die Operationen im Stream API lassen sich in zwei Typen einteilen.
- Zwischenoperationen (z. B. filter, map, distinct) — beschreiben die Verarbeitungsschritte. Sie geben einen neuen Stream zurück, stoßen für sich genommen jedoch nichts an.
- Terminaloperationen (z. B. collect, forEach, count) — starten die Pipeline und liefern ein Ergebnis.
Ein Stream arbeitet „lazy“: Solange keine Terminaloperation aufgerufen wird, passiert keine Berechnung. Deshalb beenden wir eine Kette oft mit collect(...) — das ist der Punkt, an dem der Stream wieder in eine Collection oder ein anderes Ergebnis überführt wird.
2. Operation filter: Elemente nach einer Bedingung filtern
filter ist eine Zwischenoperation, die nur diejenigen Elemente passieren lässt, die der vorgegebenen Bedingung entsprechen.
Signatur
Stream<T> filter(Predicate<? super T> predicate);
Predicate ist ein funktionales Interface, das ein Element entgegennimmt und true (behalten) oder false (aussortieren) zurückgibt.
Beispiel: nur gerade Zahlen behalten
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4, 6]
Was passiert hier?
- n -> n % 2 == 0 — ein Lambda-Ausdruck, der prüft, ob die Zahl ohne Rest durch 2 teilbar ist.
- filter behält nur die geraden Zahlen.
Beispiel: Namen filtern, die mit „A“ beginnen
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "John");
List<String> aNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(aNames); // [Anna, Alex, Alina]
Wichtig: filter verändert die Collection nicht — es wird ein neuer Stream erzeugt, der nur die benötigten Elemente enthält.
3. Operation map: ein Element in etwas anderes transformieren
map ist eine Transformationsoperation. Sie nimmt jedes Element des Streams, wendet darauf eine Funktion an und gibt ein neues Element zurück.
Signatur
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Function ist ein Interface, das ein Element annimmt und etwas zurückgibt (möglicherweise eines anderen Typs).
Beispiel: die Längen der Strings ermitteln
List<String> names = List.of("Anna", "Boris", "Alex");
List<Integer> nameLengths = names.stream()
.map(name -> name.length())
.collect(Collectors.toList());
System.out.println(nameLengths); // [4, 5, 4]
Was passiert hier?
- map verwandelt den String in seine Länge (name -> name.length()).
- Ergebnis ist ein Stream von Zahlen.
Beispiel: Strings in Großbuchstaben umwandeln
List<String> names = List.of("Anna", "Boris", "Alex");
List<String> upperNames = names.stream()
.map(name -> name.toUpperCase())
.collect(Collectors.toList());
System.out.println(upperNames); // [ANNA, BORIS, ALEX]
4. Operation collect: Ergebnis zurück in eine Collection sammeln
collect ist eine Terminaloperation, d. h. sie beendet die Arbeit des Streams und sammelt das Ergebnis in einer Collection oder einem anderen Container.
Signatur
<R, A> R collect(Collector<? super T, A, R> collector)
Keine Angst vor der „furchteinflößenden“ Signatur! In 99 % der Fälle verwenden Sie fertige Collector-Implementierungen aus der Klasse Collectors.
Collectors ist eine Utility-Klasse mit einer Sammlung von „Sammlern“. Sie sagt dem Stream, in welche Form das Ergebnis gesammelt werden soll: Liste, Menge, String usw.
Beispiele:
- Collectors.toList() — in eine List
- Collectors.toSet() — in ein Set
- Collectors.joining(", ") — in einen String, durch Kommas getrennt
Mit anderen Worten: Collectors ist wie ein Satz unterschiedlich geformter Schachteln, in die Sie die Stream-Elemente verpacken.
Beispiel: Ergebnis in eine List sammeln
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
Beispiel: Ergebnis in ein Set sammeln
Set<String> uniqueNames = names.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
Beispiel: Strings mit Komma verketten
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result); // Anna, Boris, Alex
5. Operationskette: Filtern + Transformieren + Ergebnis sammeln
Die größte Stärke des Stream API — ist die Möglichkeit, Operationen aneinanderzuketten.
Beispiel: Längen der Namen ermitteln, die mit „A“ beginnen
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "John");
List<Integer> aNameLengths = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::length)
.collect(Collectors.toList());
System.out.println(aNameLengths); // [4, 4, 5]
Schritt für Schritt:
- .stream() — wir erzeugen einen Stream aus der Liste.
- .filter(name -> name.startsWith("A")) — wir behalten nur Namen, die mit "A" beginnen.
- .map(String::length) — wir wandeln jeden Namen in seine Länge um.
- .collect(Collectors.toList()) — wir sammeln das Ergebnis in einer Liste.
Äquivalenter imperativer Code
So würde dasselbe „auf die alte Art“ aussehen:
List<Integer> result = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
result.add(name.length());
}
}
Vergleich: Stream API — eine Zeile, und es liest sich wie „was wir tun“ statt „wie wir es tun“.
6. Praxis: einige kurze Aufgaben
Üben wir! Alle Beispiele können in einer einzigen Datei ausgeführt werden — ändern Sie einfach die Daten.
Aufgabe 1: Nur ungerade Zahlen behalten und quadrieren
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
List<Integer> oddSquares = numbers.stream()
.filter(n -> n % 2 != 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(oddSquares); // [1, 9, 25, 49]
Aufgabe 2: Aus einer Liste von Strings die Liste ihrer Anfangsbuchstaben erzeugen
List<String> names = List.of("Anna", "Boris", "Alex");
List<Character> initials = names.stream()
.map(name -> name.charAt(0))
.collect(Collectors.toList());
System.out.println(initials); // [A, B, A]
Aufgabe 3: Strings mit Länge größer als 3 filtern und in ein Set sammeln
List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");
Set<String> longWords = words.stream()
.filter(word -> word.length() > 3)
.collect(Collectors.toSet());
System.out.println(longWords); // [bear, elephant]
7. Typische Fehler im Umgang mit filter, map, collect
Fehler Nr. 1: collect vergessen — kein Ergebnis!
Das Stream API ist träge wie eine Katze auf dem Fensterbrett: Solange Sie keine Terminaloperation (z. B. collect oder forEach) aufrufen, passiert nichts. Wenn Sie nur users.stream().filter(...).map(...); schreiben, wird nichts ausgeführt.
Fehler Nr. 2: filter und map vertauscht
Manche Einsteiger machen zuerst map und dann filter. Zum Beispiel, names.stream().map(String::length).filter(len -> len > 3) — das liefert Zahlen statt Strings. Wenn Sie Strings mit Länge größer als 3 brauchen, filtern Sie zuerst und transformieren danach.
Fehler Nr. 3: Unveränderlichkeit vergessen
Operationen des Stream API ändern die ursprüngliche Collection nicht! Sie liefern ein neues Ergebnis. Nach List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); — bleibt die Collection names unverändert.
Fehler Nr. 4: Versuch, eine externe veränderliche Liste zu verwenden
So sollte man es nicht machen:
List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));
Besser collect verwenden — das ist sicherer und kürzer.
Fehler Nr. 5: NullPointerException
Wenn eine Collection null-Elemente enthalten kann, führt der Aufruf name.startsWith("A") auf null zu einem Fehler. Fügen Sie, wenn möglich, einen Null-Filter hinzu:
.filter(name -> name != null && name.startsWith("A"))
GO TO FULL VERSION