Stream API in Java 8

Filed in KomputerowoTags: , , ,

Strumień

Strumień (nie należy mylić tego pojęcia ze strumieniami wejścia-wyjścia)
stanowi sekwencję elementów, ale nie przechowuje tych elementów w strukturach danych, tylko konstytuuje potok (połączenie) różnych operacji na elementach. Potok operacji ma swoje źródło (można to nazwać też źródłem strumienia), który może być np. kolekcją, tablicą lub napisem (ciągiem znaków).

Operacje, łączone w potoku, mogą być:

  • pośrednie – dające w wyniku inny strumień (np. map, filter), przy czym operacje te mogą być
    – bezstanowe – niewymagające od strumienia znajomości wartości poprzednio przetwarzanych elementów (np. filter lub forEach); takie operacje są bardzo efektywne;
    – stanowe – wymagające od strumienia pamiętania stanów innych, od akurat przetwarzanego, elementów (np. sortowanie elementów strumienia);
  • terminalne – kończące przetwarzanie strumienia (np. collect i forEach);
    po wykonaniu operacji terminalnej nie można wykonać na strumieniu żadnej innej operacji;
  • skracające (short-circuit) – takie, które powodują, że wcześniejsze w potoku operacje pośrednie kończą działanie, gdy rezultat operacji skracającej jest jasny; operacje te mogą być zarówno pośrednie, jak i terminalne.

Strumienie charakteryzują się ważnymi cechami.

  • Operacje pośrednie są leniwe (lazy operations), czyli nie są wykonywane ani nie generują żadnych wartości, dopóki nie zostanie wywołana operacja terminalna, a elementy strumienia są przetwarzane tylko w takim zakresie, jaki jest potrzebny do uzyskania wymaganego wyniku (np. gdy rezultat operacji skracającej jest w pełni określony).
  • Elementy strumienia mogą być generowane ad hoc za pomocągeneratorów i iteratorów (ogólnie takie strumienie są nieskończone; w praktyce zawsze jakoś kończymy ich przetwarzanie, np. generator liczb pseudolosowych może generować nieskończoną ich sekwencję, a my ograniczamy ją do pierwszych 100).
  • Operacje na strumieniach mogą być wykonywane równolegle (w odrębnych wątkach), przy czym bardzo łatwo to możemy zadekretować (np. za pomocą metody parallel), a podziałem pracy między równolegle wykonujące się podzadania zajmie się JVM.
  • We wszystkich operacjach strumieniowych podajemy lambda-wyrażenia, co sprzyja zwięzłości i czytelności kodu.

Strumienie są obiektami klasy implementującej interfejs Stream. Strumienie możemy uzyskać m.in.:

  • od kolekcji – za pomocą metody stream() z interfejsu Collection, np. list.stream();
  • od tablic-za pomocą statycznej metody stream() z klasy Arrays, np. Arrays.stream(array);
  • od napisów (strumień znaków) – za pomocą metody chars();
  • od podanych wartości, w tym tablic obiektowych (varargs), za pomocą metody
  • Stream.of(zestaw wartości);
  • od plików (strumień wierszy pliku) – za pomocą statycznej metody lines(…) z klasy Files lub lines() z klasy BuferredReader;
  • od katalogów (strumień reprezentujący drzewo katalogowe obiektów plikowych) – za pomocą statycznej metody walk(…) lub find(…) z klasy Files;
  • od archiwów (np. ZipFile lub JarFile; tu strumień reprezentuje uporządkowane elementy archiwum entries) za pomocą metody stream();
  • od wyrażenia regularnego, reprezentowanego przez obiekt klasy Pattern – za pomocą metody splitAsStream(napis), zwracającej strumień symboli napisu, wyłuskanych za pomocą tego wyrażenia;
  • za pomocą generowania elementów metodami Stream.generate(…) lub Stream.iterate(…);
  • za pomocą statycznej metody ints() klasy Random (strumień pseudolosowych liczb całkowitych;
  • przez leniwe połączenie strumieni statyczną metodą Stream.concat(strum1, strum2).

Podstawowe operacje pośrednie:

  • map(Function f) – Zwraca strumień z elementami tego strumienia przekształconymi za pomocą podanej funkcji (transformacja elementów).
  • flatMap(Function f) – Zwraca strumień z elementami tego strumienia przekształconymi za pomocą podanej funkcji, przy czym zastosowanie tej funkcji musi dawać strumienie, a po odwzorowaniu ich elementy będą stanowić jeden strumień (nastąpi swoiste spłaszczenie, połączenie różnych zestawów danych w jeden).
  • filter(Predicate p) – Zwraca strumień elementów, dla których predykat p daje true (selekcja elementów).
  • distinct() – Zwraca strumień elementów, różniących się w sensie equals() (wybór niepowtarzających się elementów) – operacja stanowa.
  • sorted(…) – Zwraca strumień posortowanych elementów (w porządku naturalnym lub wg podanego komparatora) – operacja stanowa.
  • unordered() – Zwraca nieuporządkowany strumień.
  • limit(int n) – Zwraca strumień zawierający n pierwszych elementów tego strumienia – operacja skracająca.
  • generate(Supplier s) – Zwraca strumień elementów, z których każdy jest tworzony przez podanego „dostawcę”, np. strumień stałych lub losowych wartości.
  • iterate(T init, UnaryOperator op) – Zwraca strumień tworzony przez iteracyjne zastosowanie operatora op wobec inicjalnej wartości init.
  • substream(…) – Zwraca część strumienia (np. od 10 elementu do końca lub od 5 do 20) – operacja skracająca.
  • parallel () – Zwraca strumień, na którym operacje będą wykonywane równolegle.
  • sequential() – Zwraca strumień, na którym operacje będą wykonywane sekwencyjnie

Wybrane operacje terminalne:

●allMatch(Predicate p) – Zwraca true, jeśli wszystkie elementy strumienia spełniają warunek p, false w przeciwnym razie.
●anyMatch(Predicate p) – J.w., ale pytanie brzmi: czy jakikolwiek element strumienia spełnia warunek; jest to operacja skracająca.
●noneMatch(Predicate p) – J.w, czy żaden element strumienia nie spełnia podanego warunku – operacja skracająca.
●findAny() – Zwraca dowolny element strumienia jako obiekt typu Optional, zawierający wartość tego elementu lub wartość pustą, jeśli strumień jest pusty – operacja skracająca.
●findFirst() – Zwraca pierwszy element strumienia (jako Optional z jego wartością lub wartością pustą, gdy strumień jest pusty) – operacja skracająca.
●long count() – Zwraca liczbę elementów strumienia.
●void forEach(Consumer c) – Wykonuje podane działanie na każdym elemencie strumienia; ale kolejność przetwarzania nie musi być taka sama, jak kolejność w zestawie, na który nałożono strumień (np. elementy listy mają swoją kolejność, ale forEach na strumieniu związanym z listą może wykonywać działania w innej kolejności).
●forEachOrdered(Consumer) – j.w., ale jeśli dane, od których uzyskano strumień, mają swoją kolejność, to przy przetwarzaniu zostanie ona zachowana.
●reduce(…) – Wykonuje redukcję elementów strumienia, czyli ogólnie – na podstawie zestawu jego elementów, stosując wobec nich odpowiednie operacje akumulujące czy kombinujące, wytwarza pewną wynikową wartość (np. sumę).
●collect(…) – Wykonuje tzw. redukcję modyfikowalną, akumulującą elementy strumienia do modyfikowalnego kontenera, takiego jak kolekcja, mapa, bufor znakowy, inny strumień.
●max(…) i min(…) – Zwracają największy (najmniejszy) element strumienia w porządku określonym przez podany jako argument komparator.
●toArray(…) – Zwraca tablicę elementów strumienia.

Przetwarzanie strumieniowe

Sekwencja działań jest następująca.
Od listy metodą stream() uzyskujemy tzw. strumień (inaczej zwany sekwencją), a na danych strumienia możemy wykonywać m.in. operacje przetwarzania (metoda map), filtrowania (metoda filter) oraz uzyskiwać nowe kolekcje wyników tych operacji (metoda collect). Metodzie map podajemy lambda-wyrażenie, którego wynikiem jest przetworzenie jego parametru (działa to tak jak transform() z naszego interfejsu Transformer). Metoda zwraca strumień, na którym można wykonywać dalsze operacje. Metodzie filter podajemy lambdę przekształcającą parametr w wartość boolowską (tak jak nasz filter()). Filtrowanie zwraca strumień, który pozwala wykonywać ew. dalsze operacje tylko na tych danych, dla których wynik lambdy jest true. Możemy zatem wywoływać sekwencje map-filter, realizując kolejne etapy przetwarzania strumienia danych. Metoda collect natomiast kończy przetwarzanie strumienia i ma za parametr obiekt klasy Collector, który tworzy nową kolekcję i dodaje do niej dane ze strumienia, na rzecz którego została wywołana. W szczególności taki kolektor, budujący listę, można uzyskać za pomocą statycznej metody Collectors.toList().

Możemy zatem napisać przykład uzyskania listy kwadratów tych elementów listy src, które są mniejsze od 10:

import static java.util.stream.Collectors.toList;
// …
List<Integer> src = Arrays.asList(5, 72, 10, 11, 9);
List<Integer> target = src.stream().filter(n -> n < 10).map(n -> n * n).collect(toList());

Może trochę więcej tu kodu, niż tradycyjnie, ale za to nie musimy definiować własnych interfejsów (metody strumieniowe korzystają z interfejsów z pakietu java.util.function, które pokrywają częste przypadki użycia lambda-wyrażeń). Ale co najważniejsze mamy dużo większą swobodę działania. Możemy np. filtrować kwadraty liczb, czyli najpierw wykonać map(), a późniejfilter():

List<Integer> num = Arrays.asList(1, 3, 5, 10, 9, 12, 7);
List<Integer> out = num.stream().map(n -> n*n).filter(n -> n > 80).collect(toList());

co da w wyniku listę liczb: [100, 81, 144]

W klasie Stream nie ma bezpośredniej metody konwertującej strumień do listy jak toList() (jest konwersja do tablicy: toArray ()). Do konwersji na kolekcję strumienia stworzono metodę collect() i klasę Collector. Podobne wyniki można uzyskać także w inny sposób.

  • streamOfString.collect(Collectors.toList()); //typ listy pozostaje ten sam, to może nie wystarczyć po przetworzeniu strumienia
  • streamOfString.collect(Collectors.toCollection(ArrayList::new)); //tworzymy nową listę
  • ArrayList list = new ArrayList<>(); streamOfLetters.forEach(list::add); //dodajemy elementy do istniejącej listy
  • ArrayList myList = new ArrayList<>(); streamOfNumbers.parallel().forEachOrdered(myList::add); //ze względu na równoległość strumienia można stracić kolejność, forEachOrdered nakazuje utrzymywać kolejność elementów na liście jaka była w strumieniu.

Przykład z klasą opisującą państwa.
Załóżmy, że mamy plik z informacjami o krajach (zaczerpnięty z serwisu GeoNames,
gdzie wymienione są nie tylko niepodległe państwa, ale również regiony, które mają
odrębne kody ISO2; w informacjach tych nie ma gęstości zaludnienia, a kontynenty są
dane tylko w postaci kodów). Na wiersze tego pliku możemy nałożyć strumień i łatwo je
przetwarzać na różne sposoby.
Utwórzmy najpierw listę obiektów klasy Country. Zastosujemy metody map i collect strumieni do tworzenia listy obiektów klasy na podstawie wierszy pliku:

Path p = Paths.get(„nazwa_pliku”);
Stream<String> ls = Files.lines(p, Charset.defaultCharset());
List<Country> clist = ls.map(Country::new).collect(Collectors.toList());

Tutaj map(Country::new) tworzy strumień obiektów klasy Country (na wierszach pliku jest wywoływany jej konstruktor, który inicjuje pola na podstawie informacji ze stringa), a metoda collect posługuje się predefiniowanym obiektem-kolektorem (statyczne odwołanie do klasy Collectors), który dodaje elementy strumienia do nowo utworzonej listy. Klasa Collectors dostarcza wielu innych gotowych kolektorów do wykorzystania w metodzie collect (np. do tworzenia różnych rodzajów kolekcji, a także grupowania elementów strumienia pod kluczami w mapach).
Od utworzonej listy krajów możemy pobrać strumień i wykonywać na nim inne operacje, np. przez clist.stream().count() dowiemy się, że mamy informacje o 250 krajach. Spróbujmy teraz odnaleźć pierwszy kraj w strumieniu, którego nazwa zaczyna się na „B” i pobrać jego nazwę.

Optional<String> first = clist.stream()
.map(Country::getName) // strumień wszystkich nazw
.filter(s -> s.startsWith(„B”)) //strumień nazw na B
.findFirst(); // pierwsza nazwa na B
String nazwa = first.get();
System.out.println(nazwa);

Jako wynik findFirst() mamy Optional, bo w strumieniu może nie być żadnego elementu. Optional opakowuje uzyskaną wartość, musimy ją jeszcze pobrać za pomocą metody get(). Jednak jeśli jej nie ma (np. w filter wyszukujemy kraje o nazwie zaczynającej się na X), to wywołanie get()spowoduje wyjątek. Aby tego uniknąć, powinniśmy zastosować następująca konstrukcję:
Optional<String> first … String nazwa = first.orElse(…) której rezultatem będzie wynik get() (jeśli jest), albo podany przez nas argument metody orElse. Dobrze to widać w następującym fragmencie:

static String firstWithPrefix(Stream<Country> str, String p){
return str.map(Country::getName)
.filter(s -> s.startsWith(p))
.findFirst().orElse(„nie ma kraju na ” + p);
}
// …
List<Country> clist = …
String nazwa = firstWithPrefix(clist.stream(), „B”);
System.out.println(nazwa);
nazwa = firstWithPrefix(clist.stream(), „X”);
System.out.println(nazwa);

Wynik:
Bośnia and Herzegovina
nie ma kraju na X

główny materiał: http://pracownik.kul.pl/files/10382/public/W13.pdf