CodeGym /Kurse /JAVA 25 SELF /Träge Auswertung (lazy evaluation) in der Stream-API

Träge Auswertung (lazy evaluation) in der Stream-API

JAVA 25 SELF
Level 33, Lektion 1
Verfügbar

1. Was ist träge Auswertung?

Träge Auswertung, oder „lazy evaluation“, ist ein Prinzip, bei dem Operationen auf Daten so lange aufgeschoben werden, bis das Ergebnis wirklich benötigt wird. Im Kontext der Stream-API bedeutet das: Wenn Sie eine Verarbeitungskette über eine Collection schreiben, führt Java diese nicht sofort aus – es wartet, bis eine Terminaloperation aufgerufen wird. Erst dann wird die gesamte Kette berechnet.

Wozu ist das gut? Erstens werden Ressourcen gespart: Elemente, die am Ende nicht benötigt werden, werden einfach nicht verarbeitet. Zweitens steigt die Performance – man kann lange Ketten bauen, ohne zahlreiche Zwischen-Collections zu erstellen. Schließlich sind „Kurzschlussauswertungen“ möglich: Sobald das erste passende Element gefunden wurde, wird die weitere Verarbeitung abgebrochen.

Analogie: Stellen Sie sich einen trägen Kellner vor. Sie sagen: „Bring die Speisekarte, dann Kaffee und dann ein Stück Kuchen.“ Er nickt, macht aber nichts … bis Sie hinzufügen: „Und jetzt bring es wirklich.“ Dann geht er los – und bringt vielleicht nur den Kaffee, wenn es keinen Kuchen mehr gibt. Ungefähr so funktionieren auch träge Streams.

2. Zwischen- und Terminaloperationen

Zwischenoperationen (intermediate):

  • filter
  • map
  • sorted
  • distinct
  • peek (zum Debuggen)
  • und andere

Zwischenoperationen geben einen neuen Stream zurück, starten die Ausführung aber nicht. Sie „bauen nur den Plan“ der Verarbeitung.

Terminaloperationen (terminal):

  • collect
  • forEach
  • reduce
  • count
  • findFirst, findAny
  • anyMatch, allMatch, noneMatch
  • und andere

Nur eine Terminaloperation startet die Ausführung der gesamten Kette.

Beispiel: Ohne Terminaloperation passiert nichts

List<String> names = List.of("Alice", "Bob", "John");

names.stream()
     .filter(name -> {
         System.out.println("Filtere " + name);
         return name.startsWith("A");
     });
// Es gibt keine Ausgaben! Der obige Code baut nur die Kette auf.

Fügen wir nun eine Terminaloperation hinzu:

names.stream()
     .filter(name -> {
         System.out.println("Filtere " + name);
         return name.startsWith("A");
     })
     .forEach(System.out::println);
// Jetzt sehen wir eine Ausgabe in der Konsole!

Ergebnis:

Filtere Alice
Alice
Filtere Bob
Filtere John

Achten Sie auf die Reihenfolge: „Alice“ wird direkt nach der eigenen Filterung ausgegeben, noch bevor „Bob“ geprüft wird. Die Elemente durchlaufen die gesamte Kette der Operationen einzeln: Das erste Element geht durch filter und sofort weiter in forEach, dann das zweite und so weiter.

3. Vorteile der trägen Auswertung

Ressourceneinsparung

Träge Auswertung spart Zeit und Speicher für Elemente, die nicht benötigt werden. Wenn Sie z. B. das erste passende Objekt suchen, stoppt die Verarbeitung beim ersten Treffer.

List<String> names = List.of("Alice", "Bob", "John", "Anna");

String firstA = names.stream()
    .filter(name -> {
        System.out.println("Prüfe: " + name);
        return name.startsWith("A");
    })
    .findFirst()
    .orElse("Nicht gefunden");

System.out.println("Ergebnis: " + firstA);

Ausgabe:

Prüfe: Alice
Ergebnis: Alice

Beachten Sie: Die übrigen Elemente werden gar nicht geprüft!

Lange Ketten ohne Zwischen-Collections

Sie können viele Operationen kombinieren (filter, map, sorted usw.), ohne bei jedem Schritt Collections zu erzeugen.

List<String> names = List.of("Alice", "Bob", "John", "Anna");

List<String> result = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList(); // Ab Java 16+, früher: .collect(Collectors.toList())

Kurzschlussauswertungen

Wenn es genügt zu wissen, „gibt es ein passendes Element?“, werden die übrigen nicht geprüft:

boolean hasLongName = names.stream()
    .anyMatch(name -> {
        System.out.println("Prüfe: " + name);
        return name.length() > 10;
    });

// Wenn das erste Element lang ist, werden die übrigen nicht geprüft!

4. Beispiele: So funktioniert träge Auswertung

Beispiel 1: Ohne Terminaloperation passiert nichts

List<Integer> numbers = List.of(1, 2, 3, 4, 5);

numbers.stream()
    .filter(n -> {
        System.out.println("Filtere " + n);
        return n % 2 == 0;
    });
// Keine Ausgabe!

Beispiel 2: Kette mit Terminaloperation

numbers.stream()
    .filter(n -> {
        System.out.println("Filtere " + n);
        return n % 2 == 0;
    })
    .map(n -> {
        System.out.println("Multipliziere " + n);
        return n * 10;
    })
    .forEach(System.out::println);

Ausgabe:

Filtere 1
Filtere 2
Multipliziere 2
20
Filtere 3
Filtere 4
Multipliziere 4
40
Filtere 5

Wichtiger Hinweis: Die Operationen laufen elementweise ab: zuerst filter, dann map, dann forEach — jeweils nacheinander für jedes Element. Es sind nicht zwei separate Durchläufe („erst alles filtern, dann alles transformieren“).

Beispiel 3: peek zum Debuggen verwenden

numbers.stream()
    .filter(n -> n % 2 == 0)
    .peek(n -> System.out.println("Bestand den Filter: " + n))
    .map(n -> n * 10)
    .peek(n -> System.out.println("Nach map: " + n))
    .forEach(System.out::println);

5. Nützliche Nuancen

Vermeiden Sie Streams mit Seiteneffekten

Trägheit kann tückisch sein, wenn Sie mit einer sofortigen Ausführung rechnen. Seiteneffekte innerhalb von map, filter oder peek (Dateischreiben, Änderung externen Zustands) können in anderer Reihenfolge, nicht für alle Elemente oder ohne Terminaloperation gar nicht ausgeführt werden.

So früh wie möglich filtern

Platzieren Sie filter möglichst am Anfang der Kette, um überflüssige Elemente früh auszuschließen und den weiteren Aufwand zu reduzieren.

Sie brauchen nur das erste Ergebnis? Passende Terminaloperationen verwenden

Wenn nur das erste passende Element benötigt wird, rufen Sie findFirst oder findAny auf. Dadurch kann der Stream unmittelbar nach dem Fund stoppen.

Streams sind nicht zum Ändern der Ausgangs-Collection da

Streams sind nicht dazu gedacht, Elemente der Ausgangs-Collection hinzuzufügen oder zu entfernen. Verwenden Sie zur Modifikation andere Mechanismen.

Visualisierung der Arbeit träger Streams

List<String> words = List.of("cat", "dog", "elephant", "fox", "giraffe");

words.stream()
    .filter(w -> w.length() > 3)
    .map(String::toUpperCase)
    .forEach(System.out::println);

So läuft es ab:

Schritt cat dog elephant fox giraffe
filter
✗ ✗ ✓ ✗ ✓
map
— — ELEPHANT — GIRAFFE
forEach
— — Ausgabe — Ausgabe

Tabelle: Vergleich von eager und lazy Ansätzen

Ansatz Wann erfolgt die Verarbeitung? Speichernutzung Leistung
Eager (gierig) Sofort beim Aufruf Kann hoch sein Mitunter langsam
Lazy (träge) Nur bei Bedarf Minimal In der Regel schneller

Gieriger Ansatz – zum Beispiel, wenn Sie manuell mehrere Durchläufe über eine Collection organisieren und Zwischenlisten erstellen.
Träger Ansatz – das sind Streams: Es passiert nichts, bis ein Ergebnis benötigt wird.

6. Typische Fehler im Umgang mit trägen Streams

Fehler Nr. 1: Erwartung eines unmittelbaren Ergebnisses. Einsteiger meinen oft, dass Aufrufe von filter oder map sofort ausgeführt werden. Ohne Terminal (z. B. collect, forEach) passiert jedoch nichts – daher „funktioniert das Debuggen nicht“, „es wird nichts ausgegeben“.

Fehler Nr. 2: Seiteneffekte in Zwischenoperationen. Dateischreiben, Änderungen externer Variablen innerhalb von map/filter/peek sind schlechte Praxis. Aufgrund der Trägheit und Optimierungen können solche Aktionen unvollständig, nicht in der erwarteten Reihenfolge oder ohne Terminaloperation gar nicht ausgeführt werden.

Fehler Nr. 3: Terminaloperation vergessen. Eine Stream-Kette wurde geschrieben, aber nicht mit collect, forEach usw. abgeschlossen. Ergebnis – „Stille“.

Fehler Nr. 4: Erwartung, dass alle Elemente verarbeitet werden. Operationen wie findFirst oder anyMatch beenden die Pipeline beim ersten Ergebnis. Die übrigen Elemente werden nicht verarbeitet – daher die Verwunderung „warum hat mein println nicht für alle funktioniert?“.

Fehler Nr. 5: Streams zum Ändern der Ausgangs-Collection verwenden. Streams sind nicht zur Modifikation der Ausgangs-Collections gedacht (Hinzufügen/Entfernen von Elementen). Verwenden Sie dafür spezialisierte Methoden der Collections oder Iteratoren.

1
Aufgabe
JAVA 25 SELF, Level 33, Lektion 1
Gesperrt
Die faule Katze und ihr Abendessen: Simulation ohne Ergebnis 😼
Die faule Katze und ihr Abendessen: Simulation ohne Ergebnis 😼
1
Aufgabe
JAVA 25 SELF, Level 33, Lektion 1
Gesperrt
Suche nach einem seltenen Artefakt in der alten Schatzkammer 💎
Suche nach einem seltenen Artefakt in der alten Schatzkammer 💎
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION