CodeGym /Kurse /JAVA 25 SELF /Methoden forEach, peek: Nebeneffekte

Methoden forEach, peek: Nebeneffekte

JAVA 25 SELF
Level 30 , Lektion 3
Verfügbar

1. Methode forEach: finale Aktion an den Elementen

Bevor wir in die Details eintauchen, erinnern wir uns kurz daran, was terminale und intermediäre Stream-Operationen sind.

  • Intermediäre Operationen (z. B. filter, map, distinct, peek) – liefern einen neuen Stream zurück und führen in der Regel keine Aktionen aus, bevor eine terminale Operation aufgerufen wird.
  • Terminale Operationen (z. B. forEach, collect, count, anyMatch) – starten die Verarbeitung der Stream-Elemente und liefern ein Ergebnis zurück (oder liefern nichts zurück wie forEach).

Nach einer terminalen Operation gilt der Stream als geschlossen, und es können keine weiteren Operationen darauf angewendet werden. Das ist so, als wollte man bereits aufgegessenes Eis weiteressen – das geht nicht: Der Stream ist bereits „verbraucht“.

Schauen wir uns nun zwei wichtige Methoden für Nebeneffekte an: forEach und peek.

Was macht forEach?

forEach ist eine terminale Stream-Operation, die für jedes Element des Streams eine vorgegebene Aktion ausführt. Typische Einsätze sind Ausgabe auf den Bildschirm, Logging, Erfassen von Statistiken und andere Nebeneffekte (side effects).

Methodensignatur:

void forEach(Consumer<? super T> action)

Consumer<T> ist ein funktionales Interface, das ein Argument annimmt und nichts zurückgibt (z. B. System.out::println).

Beispiel: Alle Listenelemente ausgeben

Angenommen, wir haben eine Liste von Benutzernamen:

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

Mit dem Stream API lassen sich alle Benutzer ganz einfach ausgeben:

users.stream().forEach(System.out::println);

Ergebnis:

Anna
Boris
Alex
Alina
Dmitry

Man kann auch einen Lambda-Ausdruck verwenden:

users.stream().forEach(name -> System.out.println("Benutzer: " + name));

Ergebnis:

Benutzer: Anna
Benutzer: Boris
Benutzer: Alex
Benutzer: Alina
Benutzer: Dmitry

Wichtig: forEach schließt den Stream ab

Nach dem Aufruf von forEach wird der Stream „geschlossen“. Die Kette kann nicht fortgesetzt werden:

users.stream()
     .filter(name -> name.startsWith("A"))
     .forEach(System.out::println)
     .map(String::toUpperCase); // Fehler! Der Stream ist bereits geschlossen.

Ein weiterer Aufruf nach forEach führt zu einem Kompilierfehler: Die terminale Operation gibt void zurück, nicht einen neuen Stream.

2. Methode peek: beobachten, nicht eingreifen

peek ist eine intermediäre Operation. Sie erlaubt, zu einem bestimmten Verarbeitungsschritt für jedes Element eine Aktion auszuführen, ohne das Element zu verändern und ohne den Stream zu beenden.

Methodensignatur:

Stream<T> peek(Consumer<? super T> action)
  • peek gibt einen neuen Stream zurück, in dem für jedes Element die Aktion action ausgeführt wird.
  • Wird typischerweise für Debugging, Logging oder Monitoring des Stream-Zustands verwendet.

Beispiel: Logging nach dem Filtern

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

List<Integer> nameLengths = users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("Filter bestanden: " + name))
    .map(String::length)
    .collect(Collectors.toList());

Ausgabe in der Konsole:

Filter bestanden: Anna
Filter bestanden: Alex
Filter bestanden: Alina

Inhalt von nameLengths:

[4, 4, 5]

Wo ist peek sinnvoll?

  • Zum Debuggen der Operationskette: sehen, was in jedem Schritt passiert.
  • Zum Erfassen von Statistiken (z. B. zum Zählen der Elemente).
  • Für Logging in Zwischenstufen.

Wichtig: peek sollte nicht verwendet werden, um Elemente des Streams zu verändern. Für Transformationen gibt es map. peek bedeutet „hinsehen“, nicht „eingreifen“.

3. forEach vs. peek: worin liegt der Unterschied?

Methode Operationstyp Wann anwenden Kann die Kette fortgesetzt werden? Wofür am besten geeignet
forEach
terminal Am Ende der Stream-Verarbeitung Nein Finale Aktionen (Ausgabe, Logging, Schreiben in die Datenbank)
peek
intermediär In der Mitte der Operationskette Ja Debugging, Zwischen-Logging, Zählen

Beispiel: Unterschied in der Verwendung

// Beispiel mit forEach
users.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .forEach(System.out::println); // Hier wird der Stream abgeschlossen

// Beispiel mit peek
users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("Filter bestanden: " + name))
    .map(String::toUpperCase)
    .collect(Collectors.toList()); // Die Kette kann fortgesetzt werden

Wichtig zu beachten

  • forEach ist ein Point of no Return: Danach kann mit dem Stream nichts Weiteres gemacht werden.
  • peek garantiert keine Ausführung, wenn keine terminale Operation folgt. Besteht die Kette nur aus intermediären Operationen, passiert nichts.

4. Weniger offensichtliche Punkte: forEach ist nicht immer die beste Wahl!

Warum sollte man forEach nicht zum Ändern von Collections verwenden?

Viele Einsteiger versuchen, mit forEach Elemente einer Collection oder die Collection selbst zu ändern (z. B. Elemente zu entfernen). Das ist jedoch eine schlechte Praxis: Streams sind nicht dazu gedacht, die Ausgangs-Collections zu modifizieren.

Beispiel für eine falsche Verwendung:

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Alex"));
names.stream().forEach(name -> {
    if (name.startsWith("A")) {
        names.remove(name); // Kann zu ConcurrentModificationException führen!
    }
});

Ergebnis: Laufzeitfehler – die Collection darf während des Stream-Durchlaufs nicht verändert werden (ConcurrentModificationException).

Wofür sollte man forEach dennoch verwenden?

  • Für die Ausgabe auf den Bildschirm (z. B. das Drucken eines Berichts).
  • Für Logging.
  • Für Aufrufe externer Dienste (z. B. das Senden einer E-Mail).
  • Zum Erfassen von Statistiken (z. B. das Erhöhen eines Zählers).

5. Noch einmal zu peek: nur für Debugging!

Es ist verlockend, peek zu verwenden, um Elemente zu verändern, etwa das Alter eines Benutzers zu erhöhen:

users.stream()
    .peek(user -> user.setAge(user.getAge() + 1)) // Schlecht!
    .collect(Collectors.toList());

Warum ist das schlecht?

  • Das verletzt die Deklarativität und die „Reinheit“ des Stream API.
  • Solcher Code wird schwer wartbar und schwer testbar.
  • Nebeneffekte in einer intermediären Operation können zu schwer erkennbaren Bugs führen.

Verwenden Sie für Transformationen besser map:

List<User> olderUsers = users.stream()
    .map(user -> new User(user.getName(), user.getAge() + 1))
    .collect(Collectors.toList());

Schaubild: Unterschied zwischen forEach und peek

users.stream()
    .filter(...)         // intermediäre Operation
    .peek(...)           // intermediäre Operation, „hinsehen”
    .map(...)            // intermediäre Operation
    .forEach(...)        // terminale Operation, „ausführen”

Erklärung:
– Alles vor forEach lässt sich kombinieren, umordnen, hinzufügen.
– Nach forEach ist der Stream geschlossen.

6. Typische Fehler im Umgang mit forEach und peek

Fehler Nr. 1: peek zum Ändern von Daten verwenden. peek ist nur zum Beobachten gedacht, nicht zum Ändern der Stream-Elemente. Für Transformationen verwenden Sie map.

Fehler Nr. 2: Erwarten, dass peek immer ausgeführt wird. peek wird nur ausgeführt, wenn danach eine terminale Operation folgt (collect, forEach, count usw.). Ohne terminale Operation passiert nichts.

Fehler Nr. 3: Versuchen, den Stream nach forEach fortzusetzen. forEach ist eine terminale Operation. Danach können keine weiteren Stream-Methoden aufgerufen werden.

Fehler Nr. 4: Eine Collection innerhalb von forEach modifizieren. Die Ausgangs-Collection (Elemente löschen oder hinzufügen) während des Durchlaufs über forEach zu verändern, führt direkt zu ConcurrentModificationException.

Fehler Nr. 5: forEach statt collect zum Sammeln des Ergebnisses verwenden. Wenn Sie Elemente in eine neue Collection sammeln möchten, verwenden Sie collect(Collectors.toList()) und nicht forEach mit manuellem Hinzufügen. Das verletzt die Deklarativität und kann in Multithreading-Szenarien zu Fehlern führen.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION