Zielony Smok - logo witryny

Hierarchia klas

  • java.lang. Object
    • java.util.stream.Collectors
    • java.util.stream.StreamSupport

Hierarchia interfejsów

  • java.lang.AutoCloseable
    • java.util.stream.BaseStream<T,S>
      • java.util.stream.DoubleStream
      • java.util.stream.IntStream
      • java.util.stream.LongStream
      • java.util.stream.Stream<T>
  • java.util.stream.Collector<T,A,R>
  • java.util.function.Consumer<T>
    • java.util.stream.Stream.Builder<T>
  • java.util.function.DoubleConsumer
    • java.util.stream.DoubleStream.Builder
  • java.util.function.IntConsumer
    • java.util.stream.IntStream.Builder
  • java.util.function.LongConsumer
    • java.util.stream.LongStream.Builder

Hierarchia wyliczeń

  • java.lang.Object
    • java.lang.Enum<E> (implements java.lang.Comparable<T>, java.io.Serializable)
      • java.util.stream.Collector.Characteristics

Interfejsy

Interfejs Opis
BaseStream<T,S extends BaseStream<T,S>> Podstawowy interfejs dla strumieni, które są sekwencjami
elementów, wspomagająca wykonywanie sekwencyjnych i równoległych
operacji agregujących.
Collector<T,A,R> Operacja redukcji mutowanej, która akumuluje elementy
wejściowe w wyjściowym kontenerze mutowanym, opcjonalnie
przekształcająca zaakumulowany wynik w końcową reprezentację po
przetworzeniu wszystkich elementów wejściowowych.
DoubleStream Sekwencja elementów typu double wspomagająca wykonywanie
sekwencyjnych i równoległych operacji agregujących.
DoubleStream.Builder Mutowalny budowniczy pozwalający na przygotowanie
DoubleStream.
IntStream Sekwencja elementów typu int wspomagająca wykonywanie
sekwencyjnych i równoległych operacji agregujących.
IntStream.Builder Mutowalny budowniczy pozwalający na przygotowanie
IntStream.
LongStream Sekwencja elementów typu long wspomagająca wykonywanie
sekwencyjnych i równoległych operacji agregujących..
LongStream.Builder Mutowany budowniczy pozwalający na utworzenie LongStream.
Stream<T> Sekwencja elementów typu obiektowego wspomagająca
wykonywanie sekwencyjnych i równoległych operacji agregujących.
Stream.Builder<T> Mutowalny budowniczy pozwajalący na utworzenie Stream.

Klasy

Klasa Opis
Collectors Finalna klasa zawierająca wyłącznie statyczne metody
wykorzystujące interfejs Collector, wykonujące różne użyteczne
operacje redukcji, takie jak akumulowanie elementów w kolekcjach,
sumowanie elementów zgodnie z różnymi kryteriami, etc.
StreamSupport Finalna klasa zawierająca metody statyczne niskiego
poziomu pozwalające na tworzenie strumieni i manipulowanie nimi.

Wyliczenia

Wyliczenie Opis
Collector.Characteristics Wyliczenie właściwości Collector, które mogą być użyte do
zoptymizowania implementacji redukcji

Pakiet zawiera klasy wspierające programowanie funkcyjne i pozwalające na wykonywanie operacji na strumieniach elementów.

Strumień może zawierać elementy typów prymitywnych jak i typów obiektowych. Na wykonywanie odpowiednich operacji pozwalają klasy Stream (dla obiektów) oraz IntStream, LongStream, Double Stream (dla typów prymitywnych). Inne typy liczbowe nie są obsługiwane.

Czym są strumienie elementów?

Strumień elementów nie jest takim samym strumieniem jak strumień I/O. Strumienie nie są również kolekcjami danych. Strumienie elementów bardziej przypominają iteratory kolekcji. Strumienie różnią się od kolekcji tym, że:

  • Nie przechowują danych. Strumień nie jest strukturą danych, która przechowuje elementy. Strumień elementów jest raczej potokiem danych, pobranych (skopiowanych) ze struktury danych, tablicy, funkcji generującej, czy kanału I/O i przekazywanych dalej i na których w trakcie przepływu wykonywane są operacje obliczeniowe. Można go sobie wyobrazić jako rzeczywisty potok z danymi, z wędkarzem stojącym na mostku, rybkami w środku, sieciami do połowu, drapieżnikami selekcjonującymi rybki, śluzami, pobraniami wody i spuszczaniem ścieków do potoku, etc.
  • Operacje na elementach w strumieniu nie zmieniają elementów w strukturze danych. Przy filtrowaniu danych ze źródła, powstaje nowy strumień przefiltrowanych danych, a dane w źródle nie ulegają zmianie.
  • Operacje wykonywane na strumieniach są podzielone na operacje pośrednie i operacje końcowe (kończące). Każda z nich produkuje nowy strumień danych. Operacje pośrednie są zawsze ‘leniwe’, czyli wykonywane do spełnienia danego warunku wyszukiwania, a potem przerywane. Np. ‘znajdź pierwszą liczbę pierwszą’ powoduje zakończenie pracy od razu po znalezieniu takiej liczby.
  • Strumienie nie są ograniczone, w tym sensie, że nie posiadają określonej wartości ‘length’ czy ‘size()’ tak jak tablice czy kolekcje. Mogą być nieograniczonej wielkości. Aby móc zakończyć operacje na takim strumieniu w ograniczonym czasie wprowadzono metodę ‘limit(n)’ pozwalającą na ograniczenie liczby wykonywanych operacji.
  • ‘Rybka’ danych da się zjeść tylko raz. Aby pojawiła się ponownie należy utworzyć nowy strumień. Takie samo zachowanie wykazują iteratory po kolekcjach.

Otrzymywanie strumieni elementów

W Java 8 istnieje sporo programistycznych narzędzi do wytwarzania strumieni elementów. :

  • Metody stream() i parallelStream() interfejsu Collection
  • Metoda Arrays.stream(Object[])
  • Statyczne metody fabryczne klas strumieni (Stream.of(Object), IntStream.range(int, int),
    Stream.iterate(Object, UnaryOperator), etc
  • Metoda BufferedReader.lines() – pobierająca z pliku linie tekstu
  • Metody klasy Files – tworzące strumienie ścieżek plikowych
  • Metoda Random.ints() – tworząca strumienie liczb losowych
  • Wiele innych metod jak np. BitSet.stream(), Pattern.splitAsStream(CharSequence), JarFile.stream()

Operacje na strumieniach oraz potoki

Operacje na strumieniach elementów są podzielone na pośrednie i końcowe (kończące). Operacje są łączone w potoki. Potok składa się ze źródła danych (kolekcja, tablica, funkcja generująca, kanał I/O), po którym następuje zero lub więcej operacji pośrednich, na przykład Stream.filter, czy Stream.map a potem operacja kończąca na przykład Stream.forEach, czy Stream.reduce.

Operacje pośrednie zawsze tworzą i zwracają nowy strumień. Zawsze są ‘leniwe’. Np. operacja filter() powinna utworzyć nowy strumień zawierający dane przefiltrowane zgodnie z warunkiem. Ale ponieważ jest ‘leniwa’ żadne trawersowanie po danych, ani filtrowanie nie jest wykonywane. Tworzona jest jedynie kopia całego źródła danych. Trawersowanie danych zgodne z warunkiem, powodujące ograniczenie liczby danych w nowym strumieniu, jest wykonywane dopiero po wywołaniu operacji kończącej.

Gdy operacja kończąca jest wykonana, potok uważany jest za pochłonięty i nie może być dalej używany.

Jeżeli zajdzie potrzeba ponownego przetwarzania danych należy utworzyć nowy strumień ze źródła danych.

Operacje kończące są zawsze ‘gorliwe’. Wykonują operacje natychmiast po wywołaniu i przed powrotem do miejsca wywołania. Wyjątkiem są operacje kończące iterator() i spliterator(), które wydają się być ‘gorliwe’, gdyż są zaplanowane jako swoiste ‘luki bezpieczeństwa’ mające umożliwić wykonywanie operacji wymaganych przez programistów, gdy istniejące operacje nie będą wystarczające do wykonania danego zadania.

‘Leniwe’ przetwarzanie pozwala na znaczne zwiększenie efektywności wykonywania operacji. Wiele operacje np. sumowania, filtrowania, mapowania mogą być wykonane przy jednym trawersowaniu danych przy minimalnej liczbie stanów pośrednich. Ponadto, przy niektórych warunkach np. znajdź pierwsze wystąpienie słowa zawierającego ‘aa’ pozwala na zakończenie wszystkich operacji bezpośrednio po znalezieniu szukanego łańcucha znaków.

Operacje pośrednie są dzielone na operacje stanowe i bezstanowe.

Operacje bezstanowe, na przykład filter i map nie zachowują informacji o stanach elementów poprzednich przy przetwarzaniu elementów następnych. W operacjach stanowych, na przykład distinct i sorted informacja o stanie już przetworzonych elementów jest zachowywana i może wpływać na przeprowadzaną informację.

W przetwarzaniu równoległym niektóre potoki zawierające operacje stanowe mogą wymagać kilku przebiegów albo mogą wymagać buforowania istotnych danych. Jeśli potok zawiera wyłącznie pośrednie operacje bezstanowe mogą one być wykonane w jednym przebiegu, ale inne mogą wymagać buforowania danych i więcej niż jednego przebiegu.

Niektóre operacje są uznane za krótkospięciowe. Operacja pośrednia jest krótkospięciowa, jeśli użyta na nieskończonym źródle danych, pozwala uzyskać w wyniku skończony strumień elementów. Operacja kończąca jest uważana za krótkospięciową jeśli zastosowana do nieskończonego źródła danych pozwala na zakończenie w skończonym czasie.

Wystąpienie operacji krótkospięciowej w potoku jest warunkiem koniecznym, ale nie wystarczającym do przetworzenia nieskończonego strumienia tak, aby zakończyć go normalnie w skończonym czasie.

Równoległość

Operacje wykonywane po wywołaniu na przykład pętli for są operacjami szeregowymi wykonywanymi kolejno na elementach. Operacje na strumieniach mogą być operacjami zarówno szeregowymi jak i równoległymi (zorientowanymi szeregowo albo równolegle). Strumienie ułatwiają wykonywanie równoległe dzięki rozłożeniu operacji na części i złożenie ich w potok operacji agregujących. Wszystkie operacje w JDK tworzą domyślnie strumienie szeregowe, chyba, że z góry zażądamy paralelizacji. W przypadku interfejsu Collection możemy użyć metody Collection.stream() albo metody Collection.parallelStream(). Każdy strumień utworzony jako szeregowy może być przekształcony na równoległy (lub odwrotnie) dzięki użyciu odpowiednich metod odpowiedniego interfejsu.

Po zainicjowaniu operacji kończącej potok jest wykonywany sekwencyjnie lub równolegle zależnie od orientacji strumienia (wywołanej metody). Metoda isParallel() pozwala na określenie orientacji strumienia.

Operacje wykonywane szeregowo i wykonywane równolegle, na tych amych danych, na ogół dają te same wyniki po wykonaniu. Wyjątkiem są operacje, które z założenia są niedeterministyczne takie jak na przykład findAny() – w tym wypadku wynik może być odmienny przy odmiennej orientacji strumienia.

Większość operacji na strumieniach akceptuje parametry, opisujące zachowania specyficzne dla użytkownika, które często są wyrażeniami lambda. Aby zabezpieczyć prawidłowe zachowania, parametry te nie mogą podlegać interferencji (muszą być nie zakłócone) i w większości przypadków muszą być bezstanowe. Parametry behawioralne są zawsze instancjami interfejsów funkcyjnych takich jak Function i często są wyrażeniami lambda lub referencjami do metod.

Zapobieganie interferencji

Strumienie umożliwiają wykonywanie potencjalnie równoległych operacji na bardzo wielu różnych źródłach danych włącznie z kolekcjami, które nie są bezpieczne wątkowo, takimi jak na przykład ArrayList.

Jest to możliwe tylko wówczas, gdy zapobiegnie się interferencji (zakłócaniu) danych. Chodzi o to, że źródło danych, w czasie wykonywania operacji w potoku, w ogóle nie może podlegać żadnym zmianom. Dotyczy to zarówno obliczeń równoległych jak i szeregowych. Jedynym wyjątkiem są strumienie, których źródłem są kolekcje współbieżne.

Parametry behawioralne są uważane za interferujące z niewspółbieżnymi źródłami danych, jeśli zmieniają lub wywołują modyfikację źródła danych. Jeśli interferują, mogą powodować wyjątki, błędy, nieprawidłowe odpowiedzi oraz nieprawidłowe zachowania.

Wszystkie zmiany danych w strumieniu muszą być dokonane przed uruchomieniem metod pośrednich i kończących.

Zachowania bezstanowe

Wyniki otrzymywane w potokach mogą być niedeterministyczne albo niepoprawne jeśli parametry behawioralne dla operacji strumieniowych są stanowe. Stanowe (Zależne od stanu) wyrażenie lambda (czy inny obiekt implementujący odpowiedni interfejs funkcyjny) to takie wyrażenie, którego wynik zależy od stanu, który może się zmienić podczas wykonywania działań. Bezstanowe (Niezależne od stanu) wyrażenie lambda zawsze da ten sam wynik.

Efekty uboczne

(Efektem ubocznym jest wszystko co metoda robi poza obliczeniami i zwracaniem wartości, np. zapis wartości, zapis do pliku, etc.). Efekty uboczne w parametrach behawioralnych są zdecydowanie odradzane, ponieważ mogą prowadzić do niechcianego pogwałcenia wymagań bezstanowości oraz zwiększenie ryzyka związanego z bezpieczeństwem wątków. Jeśli parametry behawioralne powodują efekty uboczne istnieje możliwość, że te efekty uboczne nie będą widzialne dla innych wątków. Nie ma również gwarancji, że różne operacje na tym samym elemencie, w obrębie tego samego potoku, będą wykonywane przez ten sam wątek. Nie ma również pewności co do porządku wykonywanych operacji.

Jedynie metody forEach() i peek() działają wyłącznie dzięki użyciu efektu ubocznego i powinny być używane z dużą ostrożnością.

Dokumentacja do pakietu podaje przykład, w którym metoda forEach() jest używana do dodawania elementów do kolekcji po ich przefiltrowaniu, powodując niepotrzebny efekt uboczny, który może powodować albo niewłaściwe wyniki albo utratę korzyści z paralelizacji, podczas gdy powinno się zastosować jedną z metod np. collect() pozwalającą na szybkie utworzenie kolekcji bez efektu ubocznego.

Uporządkowanie

Strumienie mają zdefiniowany porządek przebiegu (wykonywania) albo go nie mają. W obu przypadkach porządek strumienia zależy od źródła danych i operacji pośrednich. Różne źródła strumieni takie jak np. List albo tablice są z natury rzeczy uporządkowane, podczas gdy inne, takie jak hashSet nie są. Niektóre operacje pośrednie, takie jak sorted() mogą wymuszać porządek przebiegu na inaczej nieuporządkowanych strumieniach, podczas gdy inne mogą znosić uporządkowanie w inaczej uporządkowanych strumieniach (np. Basestream.unordered()). Niektóre inne metody, na przykład forEach() ignorują porządek przebiegu.

Jeśli strumień jest uporządkowany, większość operacji jest zmuszona do wykonywania działań zgodnie z kolejnością elementów. Jeśli źródło nie ma zdefiniowanego porządku przebiegu, w wyniku można otrzymać
dowolną permutację elementów.

Dla strumieni sekwencyjnych, obecność lub brak porządku przebiegu nie wpływa na wykonanie, ale na determinizm. Jeśli strumień jest uporządkowany, powtarzanie wykonywania identycznych potoków strumienia na identycznym źródle da te same wyniki. Jeśli strumień nie jest uporządkowany, powtarzanie może dawać odmienne wyniki.

W przypadku strumieni równoległych rozluźnienie ograniczeń uporządkowania może czasami pozwolić na zwiększenie efektywności. Różne operacje agregacji, takie jak odfiltrowywanie duplikatów (distinct()) albo redukcje grupujące (Collectors.groupingBy()) mogą być zaimplementowane bardziej wydajnie jeśli uporządkowanie elementów nie jest istotne.

Podobnie, operacje które z założenia muszą być wykonywane w określonym porządku, takie jak limit() mogą wymagać buforowania dla zapewnienia właściwego porządku co zmniejsza korzyści z równoległości. W przypadkach gdy stream ma narzucony porządek, ale ten porządek nie jest ważny dla użytkownika, wcześniejsze usunięcie porządku ze strumienia (unordered()) może poprawić skuteczność równoległości dla niektórych stanowych albo kończących operacji. Większość potoków będzie jednak skutecznie paralelizowana nawet przy nałożonych ograniczeniach dotyczących uporządkowania.

W następnym wpisie omówimy operacje redukcji