Zastosowanie Stream API z Java 8. Przykłady

Java Stream API jest jedną z głównych funkcjonalności dodanych w Java 8. Nie należy mylić Java 8 Streams z I/O Streams – jeden z drugim nie mają za wiele wspólnego. Java 8 Stream raczej opakowuje źródło danych umożliwiając wykonanie operacji na wspomnianym źródle danych. Stream API dostarcza szeregu metod, gdzie przetwarzanie poszczególnych elementów (np. kolekcji czy tablicy) staje się łatwe i szybkie.

Piotr Goławski. Senior Java Developer w Bosch Polska. Jego pierwsze poważne zadania programistyczne pojawiły się w 2002 roku w ramach projektów dla sektora bankowego (projekt do zarządzania przetwarzaniem płatności krajowych i zagranicznych czy też projekty wdrażające usługi cash management dla największych klientów banku). Potem były to duże projekty dla firm z branży FMCG oraz rozwiązania e-commerce dla zagranicznych instytucji finansowych oparte na technologiach Java. Miał okazję również trenować początkujących programistów na szkoleniach z Java.


Należy zaznaczyć iż strumienie jako takie nie przechowują danych i w tym sensie nie są żadną strukturą, ponadto też nie modyfikują/zmieniają źródła, na którym operują. Java 8 Stream API w znaczący sposób wykorzystuje/wspiera funkcyjny styl programowania przy wykonywaniu operacji na poszczególnych elementach strumienia np. w wypadku transformacji elementów kolekcji.

Jak to działa

Strumień w kontekście Java API zawiera sekwencje elementów, i jak wyżej wspomniałem, umożliwia wykonanie różnych operacji na elementach strumienia. Poniżej utworzymy przykładowy strumień z kilkoma elementami i powiemy trochę więcej na temat tworzenia strumieni, ich charakterystyki oraz operacji na elementach strumienia. Pokażemy tym samym, co z taką sekwencją elementów w strumieniu możemy zrobić.

Do utworzenia strumienia została użyta statyczna metoda Stream.of. Metoda ta posiada parametr varargs toteż możemy stworzyć w ten sposób strumień składający się z dowolnej liczby elementów. Warto podkreślić, że w Java 8 dodano nową metodę stream() do interfejsu Collection, stąd też taki strumień można w następujący sposób wykonać:

Oczywiście w obu wypadkach rezultat otrzymany w wyniku działania kodu będzie ten sam:

Teraz kilka słów na temat typów operacji na elementach strumienia. Generalnie możemy je podzielić na operacje pośrednie i końcowe (intermediate operations oraz terminal operations), gdzie w dalszej części będę używał angielskich odpowiedników nazw typów operacji. Operacje typu terminal są operacjami, które w wyniku działania nic nie zwracają (void), bądź też wynik działania takiej operacji nie jest już strumieniem. 

Operacje typu intermediate w wyniku działania nadal zwracają strumień w postaci sekwencji elementów, stąd też takie operacje można ze sobą łączyć bez używania średnika w kodzie. W naszym przykładzie filter, map oraz sorted są operacjami typu intermediate natomiast forEach jest operacją typu terminal.    

Dodatkową oraz ważną cechą operacji typu intermediate jest laziness (tu też będę się trzymał angielskiego terminu, bo tłumaczenie na lenistwo nie najlepiej brzmi w tym kontekście). Żeby to wyjaśnić spójrzmy na poniższy przykład, gdzie nie pojawia się operacja typu terminal.

W wyniku działania powyższego kodu nic nie zostanie wydrukowane na konsoli, ponieważ operacje typu intermediate zostaną wykonane tylko wtedy, gdy pojawi się operacja typu terminal forEach jak na poniższym przykładzie:

Tym razem na konsoli pojawi się wynik, którego się spodziewamy:

Dzięki temu podejściu streamy mogą optymalizować ilość wywołań poszczególnych metod (więcej na ten temat tutaj).

Strumienie „specjalizowane”

Tak dla przypomnienia map() może nam dostarczyć nowy strumień po wykonaniu określonej operacji dla każdego elementu oryginalnego strumienia. Nowy strumień może być strumieniem innego typu w porównaniu z oryginalnym, by krócej to ująć metoda map() przekonwertuje obiekty w strumieniu z jednego typu na inny typ. 

Warto też wspomnieć o metodzie filter(), która dostarcza nam „przefiltrowany” strumień zawierający tylko elementy z oryginalnego strumienia, spełniające określony warunek, który został wyspecyfikowany przez Predicate. 

Jak wynika z powyższego przykładu Stream (rozumiany jako obiekt) jest strumieniem (sekwencją) referencji do obiektów w naszym akurat wypadku typu String. W ogólności strumienie mogą być tworzone z różnego rodzaju źródeł danych, głównie z kolekcji – poprzez wspomnianą wyżej metodą stream() dodana do interfejsu Collection

Poza strumieniami składającymi się z „regularnych” obiektów Java 8 dostarcza specjalizowanych strumieni działających na prymitywnych typach danych typu int, long czy też double. Są to IntStream, LongStream oraz DoubleStream. Te specjalizowane strumienie okazują się przydatne, gdy mamy do czynienia z dużą ilością danych numerycznych. 

Jeśli spojrzymy na API Java 8 to okaże się, że zarówno IntStream, jak i LongStream oraz DoubleStream nie dziedziczą po interfejsie Stream, lecz po BaseStream, które jest też interfejsem nadrzędnym w hierarchii dziedziczenia w stosunku do Stream. Istotną konsekwencją tego faktu jest to iż nie wszystkie operacje/metody z API interfejsu Stream są wspierane przez implementacje dla IntStream, LongStream oraz DoubleStream

Dla przykładu, gdy spojrzymy na min() oraz max() dla Stream, dowiemy się, że metody te jako parametru formalnego używają comparatora, zaś w wypadku strumieni specjalizowanych parametr tego typu się nie pojawia. Dodatkowo warto zauważyć iż wspomniane wyżej strumienie specjalizowane wspierają dodatkowo operacje agregujące typu terminal tj. sum() oraz average()

Przykładowo by utworzyć IntStream możemy użyć metody mapToInt() dla istniejącego strumienia.

W powyższym przykładzie użyłem operacji agregującej average() typu terminal. Wynik jej działania to, jak łatwo wywnioskować po nazwie, średnia arytmetyczna wyliczona dla wszystkich elementów typu int w strumieniu. 

Możemy też w tym celu użyć statycznej metody of() by stworzyć IntStream:

lub też posłużyć statyczną metodą range(): 

Jak widzimy pierwsza wartość jest inclusive, zaś ostatnia exclusive.

Ponowne użycie strumieni w Java 8

Strumienie w Java 8 nie mogą być ponownie użyte. Wywołanie jakiejkolwiek operacji typu terminal dla strumienia spowoduje wyjątek illegalStateExcpetion, jak na poniższym przykładzie.

W powyższym przykładzie użyto dwóch operacji typu terminal anyMatch() oraz noneMatch(). Wywołanie pierwszej z nich (anyMatch()) spowodowało „zamknięcie” strumienia, a wywołanie kolejnej na „zamkniętym” strumieniu skutkowało wygenerowaniem wyjątku. 

By takie ograniczenie w pewien sposób obejść po każdej operacji typu terminal wykonanej na strumieniu należałoby utworzyć nowy strumień. W naszym wypadku możemy skorzystać z interfejsu Supplier, który został wprowadzony wraz z Java 8 (pakiet java.util.function). Interfejs ten reprezentuje funkcję, która nie używa żadnego argumentu, a wynikiem jej działania jest „wyprodukowanie” wartości typu T. Jedyną metodą zdefiniowaną dla tego interfejsu jest bezargumentowa metoda get() – interfejs ten wspiera programowanie funkcyjne. 

Na poniższym przykładzie zastosowanie tego interfejsu dla naszych potrzeb:

Jak widzimy każdorazowe wywołanie metody get() dostarcza/produkuje nam „nowy” strumień i bez problemu na takim strumieniu możemy wykonać operację typu terminal bez obaw iż zostanie rzucony wyjątek.

Zaawansowane operacje na strumieniach

Jeśli spojrzymy na Stream API Javy 8 to zauważymy jak wiele operacji (metod) jest wspieranych przez ten interfejs. W powyższych przykładach zostały wykorzystane jedne z najważniejszych i chyba najczęściej stosowanych w postaci map() i filter().

W poniższych przykładach będę korzystał do celów prezentacji z następującej definicji listy zatrudnionych. Dla przejrzystości kodu pominięte zostały settery i gettery w poniższym listingu.

Poniżej przykład konwersji strumienia danego typu na strumień innego typu – tutaj przekonwertujemy strumień typu Employee (obiektów typu Employee) na strumień typu String zawierający nazwy poszczególnych departamentów, w których są zatrudnieni  pracownicy.

Poniższy listing filtruje oryginalny strumień pod kątem pracowników, których pensja jest większa niż 2600.

Przejdźmy do nieco bardziej zaawansowanych operacji na strumieniach.

Zapraszamy do dyskusji

Patronujemy

 
 
More Stories
prawdy i mity w gamedev
Gry mobilne – prawdy i mity branży gamedev okiem Ten Square Games