CodeGym /Kurse /JAVA 25 SELF /Funktionaler Stil mit der Stream-API

Funktionaler Stil mit der Stream-API

JAVA 25 SELF
Level 49, Lektion 3
Verfügbar

1. Imperativer vs. funktionaler Stil

Beginnen wir mit einer einfachen Frage: Wozu braucht man überhaupt diesen „funktionalen Stil“? Worin ist er besser als der gewohnte, übliche Ansatz mit Schleifen? Und was bedeutet „funktionaler Stil“ in Java eigentlich?

Imperativer Stil

Imperativ bedeutet, dass Sie dem Computer sagen, wie er etwas Schritt für Schritt tun soll. Wenn Sie zum Beispiel aus einer Liste von Strings die Längen dieser Strings erhalten, nur ungerade Längen behalten und absteigend sortieren möchten, schreiben Sie ungefähr so:

List<String> words = Arrays.asList("cat", "elephant", "rhino", "tiger", "mouse");
List<Integer> lengths = new ArrayList<>();

for (String word : words) {
    int len = word.length();
    if (len % 2 != 0) {
        lengths.add(len);
    }
}
lengths.sort(Comparator.reverseOrder());
System.out.println(lengths); // [7, 5, 3]

Hier erstellen wir explizit eine Zwischenliste, fügen Elemente manuell hinzu, sortieren – alles Schritt für Schritt.

Funktionaler Stil

Funktional bedeutet, dass Sie beschreiben, was Sie bekommen möchten, nicht, wie es umgesetzt wird. In Java geschieht das über die Stream-API:

List<String> words = Arrays.asList("cat", "elephant", "rhino", "tiger", "mouse");

List<Integer> result = words.stream()
    .map(String::length)
    .filter(len -> len % 2 != 0)
    .sorted(Comparator.reverseOrder())
    .toList();

System.out.println(result); // [7, 5, 3]

Hier bauen wir gewissermaßen ein „Fließband“ zur Datenverarbeitung: Zuerst verwandeln wir Wörter in ihre Längen (map), dann filtern wir die ungeraden (filter), danach sortieren wir (sorted). All das in einer einzigen Kette – ohne explizite Zwischenkollektionen und Schleifen.

Vergleich: iterativer vs. funktionaler Stil

Im gewohnten imperativen Ansatz schreiben wir eine Schleife, in der wir dem Computer Schritt für Schritt erklären, was zu tun ist: über jedes Element iterieren, eine Bedingung prüfen, zu einer neuen Liste hinzufügen oder ausgeben. Der Code funktioniert, braucht aber mehr Zeilen, und je komplexer die Aufgabe wird, desto schwerer ist er zu lesen und zu warten.

Der funktionale Stil erlaubt es, nicht den Prozess selbst zu beschreiben, sondern das gewünschte Ergebnis. Statt einer langen Schleife bauen wir eine Kette von Operationen: filtern, transformieren, Ergebnis sammeln. Das ist kürzer, anschaulicher und verringert das Fehlerrisiko, weil weniger „Handarbeit“ mit veränderlichen Kollektionen nötig ist.

Es gibt auch eine Kehrseite. Für Einsteiger kann eine solche Kette verwirrend wirken: Mehrere Lambdas hintereinander sind schwieriger zu lesen als eine einfache Schleife. Daher punktet der funktionale Stil bei Kürze und Ausdruckskraft, erfordert aber Übung und Erfahrung.

2. Grundlegende Operationen der Stream-API

Die Stream-API ist nicht einfach nur „eine neue Art von Schleife“, sondern ein ganzes Werkzeugset für die Verarbeitung von Kollektionen im funktionalen Stil. Schauen wir uns die grundlegenden Operationen an.

Wie bekommt man einen Stream?

List<String> list = List.of("a", "bb", "ccc");
Stream<String> stream = list.stream();

Zwischenoperationen

  • map – transformiert die Elemente des Streams
  • filter – filtert Elemente nach einer Bedingung
  • flatMap – macht aus jedem Element einen Stream und „klappt“ sie auf
  • sorted – sortiert
  • distinct – entfernt Duplikate
  • limit / skip – Elemente begrenzen/überspringen

Terminaloperationen

  • forEach – führt für jedes Element eine Aktion aus
  • collect – sammelt das Ergebnis in einer Kollektion
  • reduce – reduziert den Stream auf einen Wert (z. B. eine Summe)
  • count – zählt die Elemente
  • anyMatch, allMatch, noneMatch – prüft Bedingungen

Beispiel: Verarbeitungskette

List<String> names = List.of("Anna", "Boris", "Victoria", "George", "Deborah");

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList();

System.out.println(filtered); // [BORIS, VIKA, DASHA]

Visuelle Darstellung der „Pipeline“:

[Anna, Boris, Victoria, George, Deborah]
   | filter (length>3)
[Boris, Victoria, Deborah]
   | map (toUpperCase)
[BORIS, VIKA, DASHA]
   | sorted
[BORIS, VIKA, DASHA]
   | toList

Jede Operation verändert die Ursprungskollektion nicht – es wird ein neuer Stream erzeugt.

3. Stream-Ausführung und Trägheit

Zwischen- und Terminaloperationen

Streams haben zwei Arten von Operationen. Erstens Zwischenoperationen wie map, filter oder sorted. Sie geben einen neuen Stream zurück und scheinen etwas zu versprechen, führen aber tatsächlich noch nichts aus. Zweitens Terminaloperationen, zum Beispiel forEach, collect oder reduce. Diese starten die Verarbeitung wirklich. Der Schlüsselpunkt ist: Solange Sie keine Terminaloperation aufrufen, bleibt der Stream „träge“ – die Berechnungen beginnen nicht.

Beispiel:

Stream<String> stream = List.of("a", "bb", "ccc").stream()
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    });

System.out.println("Vor forEach");
stream.forEach(System.out::println);

Ausgabe:

Vor forEach
map: a
A
map: bb
BB
map: ccc
CCC

Man sieht, dass map nicht ausgeführt wird, bis forEach startet.

Warum ist das großartig?

Dieser Ansatz erlaubt es, ohne großen Aufwand beliebig lange Transformationsketten zu bauen. Der Stream beginnt erst zu arbeiten, wenn es wirklich nötig ist. Dadurch lassen sich problemlos sehr große Datenmengen oder sogar unendliche Sequenzen verarbeiten. Außerdem spart die Trägheit Speicher und ermöglicht effizientere Berechnungen.

4. Praxis: Aufgabe „Strings → Längen → ungerade → absteigend“

Gehen wir die Aufgabe Schritt für Schritt durch: „Aus einer Liste von Strings eine Liste der Stringlängen bilden, nur ungerade Längen behalten, absteigend sortieren.“

Imperative Lösung

List<String> words = Arrays.asList("cat", "elephant", "rhino", "tiger", "mouse");
List<Integer> lengths = new ArrayList<>();

for (String word : words) {
    int len = word.length();
    if (len % 2 != 0) {
        lengths.add(len);
    }
}
lengths.sort(Comparator.reverseOrder());
System.out.println(lengths); // [7, 5, 3]

Funktionale Lösung mit der Stream-API

List<String> words = Arrays.asList("cat", "elephant", "rhino", "tiger", "mouse");

List<Integer> result = words.stream()
    .map(String::length) // Strings in ihre Längen umwandeln
    .filter(len -> len % 2 != 0) // nur ungerade Längen behalten
    .sorted(Comparator.reverseOrder()) // absteigend sortieren
    .toList(); // in eine List sammeln (Java 16+)

System.out.println(result); // [7, 5, 3]

Erläuterungen:

  • map(String::length) – für jeden String seine Länge bestimmen.
  • filter(len -> len % 2 != 0) – nur ungerade Längen behalten.
  • sorted(Comparator.reverseOrder()) – absteigend sortieren.
  • toList() – den Stream in eine neue Liste sammeln.
Analogie

Das ist, als würden Sie ein Fließband in einer Fabrik bauen: Auf jeder Station werden die Teile anders bearbeitet, und erst ganz am Ende landet alles in der Kiste.

5. Weitere Beispiele: map, filter, forEach, collect

Beispiel 1: Filtern und Ausgeben

List<String> names = List.of("Anna", "Boris", "Victoria", "George", "Deborah");

names.stream()
    .filter(name -> name.contains("a"))
    .forEach(System.out::println);
// Wird ausgeben: Anna, Deborah

Beispiel 2: Transformieren und in ein Set sammeln

Set<String> upperNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toSet());

System.out.println(upperNames); // [ANNA, BORIS, VIKA, GLEB, DASHA]

Beispiel 3: Summe der Längen aller Strings ermitteln

int totalLength = names.stream()
    .mapToInt(String::length)
    .sum();

System.out.println("Gesamtlänge: " + totalLength);

Beispiel 4: Verwendung von Predicate und Function

Predicate<String> longName = name -> name.length() > 4;
Function<String, String> greet = name -> "Hallo, " + name + "!";

names.stream()
    .filter(longName)
    .map(greet)
    .forEach(System.out::println);
// Hallo, Boris!
// Hallo, Deborah!

6. Besonderheiten des funktionalen Stils mit der Stream-API

Kein veränderlicher Zustand

Der Stil der Stream-API fördert „pure“ Funktionen – ohne Seiteneffekte. Das bedeutet, dass man in Lambdas externe Variablen möglichst nicht verändern sollte.

Schlecht:

List<String> result = new ArrayList<>();
names.stream()
    .filter(name -> name.startsWith("A"))
    .forEach(result::add); // side-effect!

Besser:

List<String> result = names.stream()
    .filter(name -> name.startsWith("A"))
    .toList();

Komposition von Operationen

Man kann sehr lange Ketten bauen, indem man map, filter, sorted und andere Methoden kombiniert. Wichtig ist, es nicht zu übertreiben: Wenn die Kette länger als der Bildschirm wird, lohnt es sich, sie in Teile zu zerlegen.

Träge Ausführung

Die Stream-API tut nichts, bevor eine Terminaloperation erreicht wird. Das spart Ressourcen und ermöglicht effiziente Verarbeitungs-Pipelines.

Unveränderlichkeit der Ausgangskollektionen

Ein Stream verändert die ursprüngliche Kollektion nicht! Alle Transformationen geben einen neuen Stream/eine neue Kollektion zurück.

7. Wann sollte man die Stream-API verwenden

Stream-API eignet sich hervorragend, wenn:

  • Sie eine Kollektion schnell verarbeiten möchten (Filtern, Transformieren, Sortieren).
  • Sie knappen, gut lesbaren Code brauchen.
  • Sie nicht manuell Zwischenkollektionen erstellen wollen.
  • Sie leicht Parallelität hinzufügen möchten (über parallelStream()).

Imperativer Stil ist manchmal vorzuziehen, wenn:

  • Komplexe Logik mit mehreren verschachtelten Schleifen und Bedingungen nötig ist.
  • Maximale Performance in kritischen Bereichen wichtig ist (die Stream-API ist manchmal etwas langsamer).
  • Mit veränderlichem Zustand gearbeitet werden muss (z. B. Elemente „in place“ aktualisieren).

8. Typische Fehler beim Arbeiten mit der Stream-API

Fehler Nr. 1: Verwendung von forEach zum Sammeln einer Kollektion. Viele Einsteiger nutzen forEach, um Elemente in eine neue Kollektion hinzuzufügen. Das ist kein funktionaler Stil! Verwenden Sie stattdessen collect oder toList().

Fehler Nr. 2: Verfrühte Optimierung. Versuchen Sie nicht sofort parallelStream() einzusetzen – Parallelität ist nur für wirklich große Kollektionen und CPU-intensive Aufgaben sinnvoll.

Fehler Nr. 3: Stream-API mit veränderlichen Kollektionen mischen. Die Stream-API setzt auf unveränderliche Daten. Verändern Sie Elemente nicht innerhalb von Lambdas.

Fehler Nr. 4: „Verlust“ des Ergebnisses. Wenn Sie eine Terminaloperation vergessen, passiert gar nichts.

Fehler Nr. 5: Zu komplexe Lambdas. Wenn ein Lambda-Ausdruck länger als ein oder zwei Zeilen wird – extrahieren Sie ihn in eine eigene Methode mit verständlichem Namen.

1
Aufgabe
JAVA 25 SELF, Level 49, Lektion 3
Gesperrt
Bewertung des "magischen Inventars" im Text-Adventure 🧙‍♂️
Bewertung des "magischen Inventars" im Text-Adventure 🧙‍♂️
1
Aufgabe
JAVA 25 SELF, Level 49, Lektion 3
Gesperrt
Analyse der Siedlungen für die geheimnisvolle Expedition 🗺️
Analyse der Siedlungen für die geheimnisvolle Expedition 🗺️
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION