
Mamy to. W końcu, po serii tekstów dorośliśmy aby zacząć korzystać z Java Stream API! Wiemy jakich interfejsów używać, a także jak za pomocą lambdy albo odniesienia stworzyć na cito porządaną metodę. Pozostało już zatem tylko jedno zadanie – zbudować swój pierwszy strumień!
Słowo „zbudować” zostało użyte nieprzypadkowo. Jeśli pamiętacie wzorzec Builder/Budowniczy to wiecie czego się spodziewać. Poszczególne metody strumienia nie zmieniają oryginalnego obiektu, tylko tworzą nowe w oparciu o poprzednika.
.stream()
.map()
.distinct()
.filter()
.sorted()
//i tak dalej
Generalnie, aby utworzyć strumień potrzebna nam będzie jakaś kolekcja (czyli obiekt klasy dziedziczącej po Collections, tj. Set, List, Stack, Queue, Map). Ale są od tego wyjątki.
Możemy stworzyć pusty strumień – do tego oczywiście nie potrzebujemy żadnego obiektu. Możemy użyć tabeli. Możemy nawet skorzystać z jakiegoś pliku, a znajdujące się w nim linie tekstu staną się automatycznie elementami naszego strumienia:
Path path = Paths.get("file.txt");
Stream<String> lines = Files.lines(path);
To samo można zrobić ze ścieżką do katalogu aby utworzyć strumień ze znajdujących się w nim plików – tak zbierałem pe-de-efy z CV w projekcie ITCandidateEvaluator:
Stream<Path> paths =
Files.list(Paths.get(directoryWithResumesInPdf)))
Możliwości jest sporo, ale dla ułatwienia, w dzisiejszych przykładach będziemy używać jedynie tradycyjnych kolekcji.

Operacje na strumieniach
IntStream intStream =
IntStream.of(1,3,3,7,8,10,100);
intStream.(?)
Strumień stworzony, wciskamy operator kropki aby podejrzeć dostępne metody… i co teraz?
Cóż, wszystko zależy od tego co chcemy osiągnąć. Jeśli nasz strumień wygląda jak w kodzie powyżej i zawiera same liczby rzeczywiste, a chcemy zmiennoprzecinkowe, to śmiało możemy je zmapować na typ 'double’. Innej metody użyjemy kiedy zależy nam aby wydobyć wszystkie liczby parzyste, a jeszcze innej aby dostać kolekcję pozbawioną jakichkolwiek duplikatów.
Przejdźmy sobie teraz przez najczęściej używane metody w javowych strumieniach:
map(Function f) & flatMap(Function f)
Dla wielu z was to będzie oczywista oczywistość, ale i tak zaznaczę, że mapa w świecie kodu to coś innego niż ten kawałek papieru, który zabieramy ze sobą na wycieczkę. Istotą mapy jest zbiór par klucz-wartość, z czego KLUCZ MUSI BYĆ UNIKALNY (choć wartości mogą się powtarzać). Dla przykładu:
<Pesel, Miasto>
<IdUżytkownika, Grupa>
<Potrawa, Smak>
<Piosenka, GatunekMuzyczny>
Stąd czasownik „zmapować” oznacza ni mniej ni więcej, tylko nadać danemu kluczowi jakąś wartość. To właśnie czyni metoda map() przyjmująca za parametr jakąś funkcję (interfejs Function). Może się ona przydać kiedy tworzymy strumień jakichś użytkowników, ale potrzebujemy np. tylko ich imienia. Wtedy robimy poniższy myk:
Stream<User> userStream = users.stream().map(User::getName)
Od tej pory dostajemy całkiem nowy strumień, ale zamiast obiektów User, mamy już tam same ciągi z imionami.
Ciekawą wersję mapowania dostarcza metoda flatMap(), gdyż potrafi ona „spłaszczyć kolekcję”. Wyobraźcie sobie, że ten obiekt User ma również zmienną typu List favouriteFoods. Jeśli spróbowalibyśmy tę listę zmapować jak poprzednio dostalibyśmy ją pogrupowaną wedle kolejnych użytkowników. Natomiast metoda flatMap() zwraca nam kolejny strumień zawierający tylko i wyłącznie elementy tych wszystkich list, bez żadnego grupowania:


filter(Predicate p)
Druga najważniejsza i najczęściej spotykana metoda strumieniowa. Bierzemy jakieś stwierdzenie (interfejs Predicate) i według niego filtrujemy elementy strumienia. Jeśli dany zawodnik wrzucony do naszego warunku zwraca 'true’ to idzie dalej, w przeciwnym wypadku zostaje na sitku. To może być np:
- imię dłuższe od 5 znaków (user -> user.getName().length() > 5)
- wiek ma być parzysty (user -> user.getAge() % 2 == 0)
- wśród ulubionych potraw musi być pizza (user -> user.getFavouriteFoods().contains(„pizza”))
- tylko kobieta (user -> user.getGender().equals(User.Gender.F))
- tylko niezamężna (user -> !user.isMarried())

sorted(Comparator c)
Skoro zmapowaliśmy sobie kolekcję na pożądany typ, potem przepuściliśmy ją przez wszystkie niezbędne filtry, to teraz wypadałoby to jeszcze posortować.
Metoda sorted() przyjmuje obiekt typu Comparator, który definiuje na jakich zasadach mamy uporządkować elementy strumienia. Czy alfabetycznie? Czy na podstawie długości zapisu bitowego? Rosnąco, a może malejąco?
Możemy użyć wersji sorted() beż żadnych parametrów – to dobrze zadziała do typów prostych i Stringów. A jeśli zapiszemy to w poniższy sposób to odwrócimy kolejność:
.sorted(Comparator.reverseOrder())
Nic jednak nie stoi na przeszkodzie aby naszych Userów uporządkować wedle własnych reguł, np.
- wedle wieku: (u1,u2) -> u1.getAge() – u2.getAge()
- wedle długości imienia: (u1,u2) -> u1.getName().length() – u2.getName().length()
- wedle imienia, alfabetycznie: (u1,u2) -> u1.getName().compareTo(u2.getName())

peek(Consumer c)
Peek() to raczej metoda pomocnicza – z angielskiego ’peek’ oznacza ’podejrzeć’ i to jest dokładnie to co ta metoda robi. Jeśli czasem coś nam nie będzie grało w strumieniu to pomiędzy pozostałymi metodami możemy walnąć sobie peek() i na przykład wypisać aktualne elementy w konsoli, albo zalogować do dziennika, albo skopiować do innej kolekcji. Cokolwiek co może zostać obsłużone Consumerem.
Peek() w żaden sposób nie modyfikuje stanu strumienia – służy tylko do podglądania.

distinct()
To metoda żywcem wyjęta z SQL’a – tam wygląda tak samo i robi dokładnie to samo czyli sprawia, że wypisane elementy się nie powtarzają. Jeśli zależy nam na unikalnej kolekcji to wstawiamy distinct() i gotowe. O ile oczywiście nie zapisujemy strumienia do Setu, bo tam i tak nie może być żadnych duplikatów i w/w metoda będzie zbyteczna.

limit(long n) & skip(long n)
Obie powyższe metody pobierają typ long jako swój parametr – limit(n) narzuca… no właśnie limit na ilość elementów strumienia. Natomiast skip(n) pomija pierwszych n elementów. Jeśli strumień ma 30 elementów i damy tam coś takiego:
.skip(10)
.limit(10)
To zostaną zwrócone elementy 11-20, razem 10 sztuk. Tylko pamiętajmy o właściwej kolejności, bo jak położymy to odwrotnie to nic nie dostaniemy.
Finiszery

Powyższe metody są jak najbardziej przydatne, ale same w sobie nie dają nam praktycznie nic. Co z tego, że zmapujemy jakieś obiekty na inne, przefiltrujemy i posortujemy, jeśli nie będzie nam dane zobaczyć rezultatów tych operacji?
Pewnym wyjściem jest oczywiście odpowiednie użycie metody forEach() aby np. dodać każdy element strumienia do już istniejącej kolekcji albo po prostu wyświetlić go w konsoli. Ale do ideału temu rozwiązaniu daleko i w zastosowaniach komercyjnych się nie sprawdzi.
Innym sposobem na zatrzymanie strumienia jest skorzystanie z jednej z funkcji kończących, dla elegancji nazwijmy je finiszerami. Takie fatality dosłownie zatrzymuje strumień, który od tej pory staje się zupełnie bezużyteczny – jakakolwiek próba reaktywacji wywali nam:
Exception in thread "main"
java.lang.IllegalStateException:
stream has already been operated upon
or closed.
Popularne funkcje kończące to:
- forEach(Consumer c) –> wykonuje zadaną operację na każdym elemencie strumienia po czym go zamyka;
- toArray() –> tworzy tabelę (typu takiego jak elementy strumienia) i zatrzymuje strumień;
- count() –> zwraca wartość typu prostego long, która jest liczbą pozostałych elementów strumienia (i, rzecz jasna, kończy pracę strumienia);
Optional<T>
Innym rodzajem finiszerów są te zwracające Optionala, który stanowi remedium na poniższą dolegliwość:

Optional to opakowanie/pudełko w które wkładamy to co zwrócił nam strumień. Może nie być tam żadnych wartości, bo wszystko utknęło na poprzedzających filtrach, ale ów obiekt-pudełko dostaniemy na 100% dzięki czemu unikniemy wyrzucenia wyjątku NullPointer. Aby dostać się do wnętrza opakowania, trzeba je oczywiście otworzyć metodą get() i tam w/w wyjątek już może wyjść, ale i na to mamy receptę:
optional.ifPresent(text ->
System.out.println(text));
Czyli: tylko jeśli w pudełku coś jest, otwórz je i zrób coś z jego zawartością (interfejs Consumer). W przeciwnym wypadku nie rób nic.
Ale, ale – nawet tu pojawia się nam fajna opcja, bo możemy jako końcową metodę strumienia dodać orElse(x), a w nawiasach zamiast x podać obiekt, który zostanie zwrócony jeśli zawartość strumienia okaże się pusta:
Z popularnych metod kończących, których efektem jest zwrócenie Optionala, warto wymienić:
- findAny() –> zwraca zazwyczaj pierwszy element strumienia
- findFirst() –> zwraca ZAWSZE pierwszy element strumienia
- reduce() –> ustawiamy BinaryOperator na podstawie którego cały zestaw elementów strumienia jest redukowany do jednego obiektu, np.
(1).reduce((t1, t2) -> t2);
(2).reduce((t1, t2) ->
{if (t1.startsWith("m")) return t1; else return t2;});
Kod (1) powyżej zawsze zwróci nam ostatni element strumienia. Natomiast (2) zostawi nam pierwszy element, który zaczyna się na literę „m”.

Collectors
Optionale sprawdzają się, kiedy zależy nam na znalezieniu pojedynczego elementu z kolekcji, albo sprawdzeniu czy w ogóle takowy tam istnieje. Jednakże, pracując z kolekcjami i przepuszczając je przez strumienie najczęściej zostaniemy na końcu z liczbą mnogą elementów. W takich sytuacjach najlepiej sprawdzają się statyczne metody klasy Collectors. Kończymy nasz strumień wywołaniem metody collect() która za parametr przyjmuje jakiegoś Kolektora:
.collect(Collectors.toSet());
.collect(Collectors.toList());
.collect(Collectors.toMap
(Function k, Function v));

Dostajemy także metodę joining, przydatną np. przy łączeniu poszczególnych elementów typu String w jeden duży obiekt String:

Bardzo fajne perspektywy roztacza przed nami metoda groupingBy() dzięki której możemy ustawić ramy (metodą interfejsu Function) wedle których elementy zbioru zostaną pogrupowane. A także jej siostra: partitioningBy(), dzieląca zbiór tylko na dwie grupy: elementy spełniające lub niespełniające zawartego w niej warunku (Predicate).


Boolean
Na koniec jeszcze metody sprawdzające strumień pod względem określonego warunku (Predicate). Kiedy np. chcemy sprawdzić, czy wszyscy nasi użytkownicy podali nr telefonu, albo czy jest tam choć jedna osoba z naszego miasta, tudzież czy nikt przypadkiem nie jest miłośnikiem pizzy z ananasem, wtedy możemy zakończyć nasz strumyk jedną z poniższych metod. Ich nazwy mówią same za siebie.
allMatch()
anyMatch()
noneMatch()

Strumienie -> Podsumowanie
Powyższe metody to oczywiście nie wszystko co oferuje Stream API, ale jak możecie zauważyć, artykuł już i tak spasł się do granic możliwości, dlatego uznajmy to za odpowiednie miejsce do zakończenia tematu.
Co pominąłem?
W sumie, nic szczególnie istotnego:
- to jak ma zachować się metoda toMap() w przypadku zduplikowanych kluczy (tip: ta metoda ma wersję biorącą 3 parametry)
- klasę IntSummaryStatistics dającą podgląd na statystyki strumienia zawierającego typ prosty int (są też wersje dla long i double)
- łączenie kilku strumieni w jeden
- pracę wielowątkową (parallelStream())
Żadna z powyższych metod nie przytrafia się zbyt często w czasie pracy ze strumieniami, a jeśli już – to mając tak solidne podstawy z pewnością sami dacie radę rozkminić jak się za zabrać za otrzymane zadanie.
Tym optymistycznym akcentem pozwolę sobie zakończyć niniejszy cykl poświęcony programowaniu funkcyjnemu w Javie.
Teraz kilka dni przerwy i bierzemy się za coś jeszcze lepszego!
