Potoki zamiast pętli
Strumień (stream) to potok do przetwarzania sekwencji wartości. Zaczynasz od źródła, zwykle kolekcji, łączysz operacje w rodzaju "zostaw tylko te", "przekształć każdy", "posortuj je", a na końcu zbierasz wynik. Zamiast pisać pętlę z tymczasową listą i if w środku, opisujesz, czego chcesz, a przechodzenie po elementach zostawiasz strumieniowi.
Skoro znasz już lambdy, strumienie wskakują na swoje miejsce: każda operacja przyjmuje lambdę (albo referencję do metody) opisującą pracę dla jednego elementu.
Czytaj od góry do dołu: weź imiona, zostaw długie, zamień na wielkie litery, zbierz do listy. Oryginalna lista names nigdy nie jest zmieniana: strumień tworzy nowy wynik, a źródło zostawia w spokoju.
Budowa strumienia
Każdy potok ma trzy części:
- Źródło:
list.stream(),Arrays.stream(array)alboStream.of(a, b, c). - Zero lub więcej operacji pośrednich:
filter,map,sorted,distinct,limit. Każda zwraca kolejny strumień, więc można je łączyć w łańcuch. - Dokładnie jedna operacja końcowa:
collect,count,forEach,reduce,findFirst. Uruchamia pracę i daje wynik (albo efekt uboczny).
Ten podział ma znaczenie z powodu jednej reguły, która na początku zaskakuje każdego.
Operacje pośrednie są leniwe
Operacje pośrednie same z siebie nic nie robią. Tylko zapisują, co ma się stać. Potok uruchamia się dopiero wtedy, gdy operacja końcowa poprosi o wynik.
Uruchom to: lambda z map nigdy się nie wykonuje. Dodaj operację końcową, a cały łańcuch ożywa:
Ta leniwość to nie dziwactwo, z którym trzeba walczyć: pozwala strumieniowi łączyć operacje i pomijać niepotrzebną pracę. Oznacza jednak, że potok bez operacji końcowej nic nie robi, co jest częstym błędem typu "dlaczego nic się nie dzieje?".
filter i map: zostaw i przekształć
Te dwie operacje dźwigają większość pracy. filter przyjmuje Predicate (lambdę zwracającą boolean) i zostawia elementy, które przejdą test. map przyjmuje Function i zastępuje każdy element wynikiem jej zastosowania.
Zwróć uwagę, że map może zmienić typ elementów: tutaj strumień String staje się strumieniem Integer. String::length to referencja do metody, czyli skrót lambdy w -> w.length().
Operacje końcowe dają wynik
Gdy strumień ma już właściwy kształt, operacja końcowa zamienia go w coś konkretnego:
Popularne operacje końcowe: collect (zebranie do listy, zbioru lub mapy), count, forEach, anyMatch / allMatch / noneMatch, findFirst, min / max i reduce. Po wykonaniu operacji końcowej strumień jest zużyty i nie da się go użyć ponownie. Wywołaj jeszcze raz list.stream(), żeby dostać nowy potok.
Zbieranie wyników
collect z Collectors to główne narzędzie do budowania wyniku. Najczęściej używa się Collectors.toList():
Collectors daje też toSet(), joining(...), groupingBy(...) i counting(). Od Javy 16 możesz zastąpić collect(Collectors.toList()) krótszym .toList() (zwraca listę niemodyfikowalną):
List<String> result = names.stream().map(String::toUpperCase).toList();
sorted, distinct i limit
Te operacje pośrednie zmieniają kształt strumienia przed zebraniem wyniku:
sorted() bez argumentu używa porządku naturalnego; aby ustalić własną kolejność, przekaż Comparator (tutaj lambdę). Comparator.reverseOrder() to czytelniejszy sposób sortowania malejąco.
reduce: zwijanie strumienia do jednej wartości
Gdy trzeba połączyć wszystkie elementy w jeden wynik, np. sumę, iloczyn albo najdłuższy napis, ogólnym narzędziem jest reduce. Podajesz mu wartość początkową i funkcję, która łączy dwie wartości:
Do zwykłych sum i średnich czytelniejsze są strumienie wyspecjalizowane: nums.stream().mapToInt(Integer::intValue).sum(). Po reduce sięgaj, gdy nie ma gotowej funkcji agregującej.
Strumienie nie zastępują każdej pętli
Strumienie błyszczą, gdy przekształcasz kolekcję w wynik. Nie są automatycznie szybsze od pętli i stają się niewygodne, gdy trzeba zmieniać zewnętrzny stan albo przerywać pętlę w zawiły sposób. Dobra zasada: jeśli potok czyta się jak jedno jasne zdanie, użyj strumienia; jeśli sięgasz po współdzielony licznik albo index, zwykła pętla for jest uczciwym i dobrym wyborem.
Pamiętaj też, że strumień jest jednorazowy. Ponowne działanie na nim rzuca wyjątek:
Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count(); // IllegalStateException: stream has already been operated upon
Dalej: Optional
Kilka operacji końcowych, takich jak findFirst, min, max czy reduce bez wartości początkowej, może nic nie znaleźć, więc nie zwracają gołej wartości. Zwracają Optional, kontener Javy na "może wartość, a może nic", który wreszcie daje czystą alternatywę dla zwracania null. O nim jest następna strona.
Najczęściej zadawane pytania
Czym jest strumień (stream) w Javie?
Strumień to potok do przetwarzania sekwencji elementów, zwykle pochodzących z kolekcji, przez łańcuch operacji takich jak filter, map i sorted, zakończony operacją końcową, np. collect albo count. Nie przechowuje danych i nie zmienia źródła; opisuje obliczenie. Tworzysz go przez list.stream() i czytasz łańcuch od góry do dołu jak przepis.
Jak zamienić strumień z powrotem na listę w Javie?
Zakończ potok kolektorem końcowym: list.stream().filter(...).collect(Collectors.toList()). Od Javy 16 możesz użyć krótszego .toList(), które zwraca listę niemodyfikowalną. Bez operacji końcowej nic się nie wykona, bo operacje pośrednie, takie jak filter i map, są leniwe.
Kiedy używać strumienia zamiast pętli for?
Sięgaj po strumień, gdy przekształcasz albo filtrujesz kolekcję do wyniku: potok czyta się jak jedno jasne zdanie ("weź imiona, zostaw długie, zamień na wielkie litery, zbierz do listy"). Zostań przy zwykłej pętli for, gdy musisz zmieniać zewnętrzny stan, przerywać pętlę w skomplikowany sposób albo logika jest prostsza jako kroki imperatywne. Strumienie służą czytelności, a nie surowej szybkości.