1. Metoda forEach: działanie finalne na elementach
Zanim zanurzymy się w szczegóły, przypomnijmy sobie, czym są operacje terminalne i pośrednie strumienia.
- Operacje pośrednie (np. filter, map, distinct, peek) — zwracają nowy strumień i zazwyczaj nie wykonują żadnych działań aż do wywołania operacji terminalnej.
- Operacje terminalne (np. forEach, collect, count, anyMatch) — uruchamiają przetwarzanie elementów strumienia i zwracają wynik (albo nic nie zwracają, jak forEach).
Po operacji terminalnej strumień jest uznawany za zamknięty i nie można już stosować do niego innych operacji. To jak próbować dojeść już zjedzone lody — nie da się: strumień został już „wykorzystany”.
Poznajmy teraz dwa ważne metody do pracy z efektami ubocznymi: forEach i peek.
Co robi forEach?
forEach to operacja terminalna strumienia, która wykonuje określone działanie dla każdego elementu strumienia. Zwykle służy do wypisywania na ekran, logowania, zliczania statystyk i innych efektów ubocznych (side effects).
Sygnatura metody:
void forEach(Consumer<? super T> action)
Consumer<T> — to interfejs funkcyjny przyjmujący jeden argument i nic nie zwracający (np. System.out::println).
Przykład: wypisanie wszystkich elementów listy
Załóżmy, że mamy listę imion użytkowników:
List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");
Wypisanie wszystkich użytkowników na ekran za pomocą Stream API jest bardzo proste:
users.stream().forEach(System.out::println);
Wynik:
Anna
Boris
Alex
Alina
Dmitry
Można użyć też wyrażenia lambda:
users.stream().forEach(name -> System.out.println("Użytkownik: " + name));
Wynik:
Użytkownik: Anna
Użytkownik: Boris
Użytkownik: Alex
Użytkownik: Alina
Użytkownik: Dmitry
Ważne: forEach zamyka strumień
Po wywołaniu forEach strumień się „zamyka”. Nie można kontynuować łańcucha:
users.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println)
.map(String::toUpperCase); // Błąd! Strumień jest już zamknięty.
Próba wywołania czegokolwiek po forEach doprowadzi do błędu kompilacji: operacja terminalna zwraca void, a nie nowy strumień.
2. Metoda peek: podglądamy, ale nie ingerujemy
peek to operacja pośrednia. Pozwala wykonać działanie dla każdego elementu na określonym etapie przetwarzania, nie zmieniając samego elementu i nie kończąc strumienia.
Sygnatura metody:
Stream<T> peek(Consumer<? super T> action)
- peek zwraca nowy strumień, w którym dla każdego elementu zostanie wykonane działanie action.
- Zwykle używa się jej do debugowania, logowania lub monitorowania stanu strumienia.
Przykład: logowanie po filtracji
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("Przeszedł filtr: " + name))
.map(String::length)
.collect(Collectors.toList());
Wynik w konsoli:
Przeszedł filtr: Anna
Przeszedł filtr: Alex
Przeszedł filtr: Alina
Zawartość nameLengths:
[4, 4, 5]
Gdzie warto używać peek?
- Do debugowania łańcucha operacji: podejrzenie, co dzieje się na każdym etapie.
- Do zbierania statystyk (np. zliczania liczby elementów).
- Do logowania danych na etapach pośrednich.
Ważne: peek nie powinien być używany do modyfikowania elementów strumienia. Do przekształceń służy map. peek to „podejrzeć”, a nie „ingerować”.
3. forEach vs peek: jaka jest różnica?
| Metoda | Typ operacji | Kiedy stosować | Czy można kontynuować łańcuch? | Do czego najlepiej się nadaje |
|---|---|---|---|---|
|
Terminalna | Na samym końcu przetwarzania strumienia | Nie | Działania finalne (wyświetlanie, logowanie, zapis do bazy danych) |
|
Pośrednia | W środku łańcucha operacji | Tak | Debugowanie, logowanie pośrednie, zliczanie |
Przykład: różnica w użyciu
// Przykład z forEach
users.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println); // Tutaj strumień się kończy
// Przykład z peek
users.stream()
.filter(name -> name.startsWith("A"))
.peek(name -> System.out.println("Filtr przeszedł: " + name))
.map(String::toUpperCase)
.collect(Collectors.toList()); // Można kontynuować łańcuch
Warto pamiętać
- forEach to punkt bez powrotu: po niej nie da się już nic zrobić ze strumieniem.
- peek nie gwarantuje wykonania działań, jeśli nie zostanie wywołana operacja terminalna. Jeśli napiszemy tylko łańcuch z operacji pośrednich — nic się nie wydarzy.
4. Nieoczywiste kwestie: forEach nie zawsze jest najlepszym wyborem!
Dlaczego nie należy używać forEach do modyfikowania kolekcji?
Wielu początkujących próbuje za pomocą forEach zmieniać elementy kolekcji lub samą kolekcję (np. usuwać elementy). To jednak zła praktyka: strumienie nie są przeznaczone do modyfikowania kolekcji źródłowej.
Przykład nieprawidłowego użycia:
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Alex"));
names.stream().forEach(name -> {
if (name.startsWith("A")) {
names.remove(name); // Może prowadzić do ConcurrentModificationException!
}
});
Wynik: błąd w czasie wykonywania — nie wolno modyfikować kolekcji podczas iteracji strumienia (ConcurrentModificationException).
Do czego jednak używać forEach?
- Do wyświetlania na ekranie (np. druk raportu).
- Do zapisu do logów.
- Do wywołań usług zewnętrznych (np. wysyłka e‑maili).
- Do zbierania statystyk (np. zwiększanie licznika).
5. Jeszcze raz o peek: tylko do debugowania!
Kusi, by używać peek do modyfikowania elementów, np. zwiększyć wiek użytkownika:
users.stream()
.peek(user -> user.setAge(user.getAge() + 1)) // Źle!
.collect(Collectors.toList());
Dlaczego to źle?
- To narusza deklaratywność i „czystość” Stream API.
- Taki kod staje się trudny w utrzymaniu i testowaniu.
- Efekty uboczne w operacji pośredniej mogą prowadzić do nieoczywistych błędów.
Lepiej użyć map do przekształcania danych:
List<User> olderUsers = users.stream()
.map(user -> new User(user.getName(), user.getAge() + 1))
.collect(Collectors.toList());
Schemat: różnica między forEach a peek
users.stream()
.filter(...) // operacja pośrednia
.peek(...) // operacja pośrednia, "podglądamy"
.map(...) // operacja pośrednia
.forEach(...) // operacja terminalna, "wykonujemy działanie"
Wyjaśnienie:
— Wszystko, co przed forEach, można łączyć, przestawiać, dodawać.
— Po forEach strumień jest zamknięty.
6. Typowe błędy przy pracy z forEach i peek
Błąd nr 1: używanie peek do modyfikowania danych. peek służy wyłącznie do obserwacji, a nie do zmiany elementów strumienia. Do przekształceń używaj map.
Błąd nr 2: oczekiwanie, że peek zawsze się wykona. peek wykonuje się tylko wtedy, gdy po nim występuje operacja terminalna (collect, forEach, count itd.). Bez operacji terminalnej nic się nie wydarzy.
Błąd nr 3: próba kontynuowania strumienia po forEach. forEach to operacja terminalna. Po niej nie można wywoływać innych metod strumienia.
Błąd nr 4: modyfikowanie kolekcji wewnątrz forEach. Zmiana kolekcji źródłowej (usuwanie lub dodawanie elementów) podczas przechodzenia za pomocą forEach to prosta droga do ConcurrentModificationException.
Błąd nr 5: używanie forEach zamiast collect do zebrania wyniku. Jeśli chcesz zebrać elementy do nowej kolekcji, użyj collect(Collectors.toList()), a nie forEach z ręcznym dodawaniem. To narusza deklaratywność i może prowadzić do błędów w scenariuszach wielowątkowych.
GO TO FULL VERSION