Strumienie

W 8 wersji Javy pojawiły się strumienie. Pozwalają przetwarzać dane w postaci strumieni wykonując na nich np filtracje, agregacje, usuwać duplikaty etc. Słowem wiele rzeczy do wykonania których używa się języka SQL. Możemy na przykład zamienić kolekcję na strumień, zastosować na nim filtry i ponownie rzutować na kolekcję. Na potrzeby przetwarzania strumieni w pakiecie java.util.stream dostarczono interfejs Stream<T>.

Operacje na strumieniach mogą być pośrednie lub terminalne. Operacje pośrednie to takie które są wykonywane na strumieniu "w locie" ale go nie kończą. Operacje terminalne kończą strumień. Operacji pośrednich możemy wykonać na danym strumieniu wiele – w przeciwieństwie do operacji terminalnych.

 

Rzutowanie kolekcji na strumień i strumieni na kolekcje

Zaczniemy od bazowego najprostszego przykładu. Zwykle będziemy chcieli zamienić listę na strumień, dokonać na strumieniu jakichś operacji by na koniec znowu ze strumienia odzyskać kolekcję. W poniższym przykładzie tworzę kolekcję elementów tekstowych, następnie za pomocą metody stream (wbudowanej w kolekcję) konwertuję kolekcję na strumień. W ostatniej linii ze strumienia odzyskuję kolekcję:

 

List<String> listaSlow = Arrays.asList("Koń","Krowa","Nietoperz","Niedźwiedź","Pies");

Stream<String> strumien = listaSlow.stream();

List<String> wynik = strumien.collect(Collectors.toList());

Istnieje także możliwość konwersji strumienia na Set i Map – odpowiednio metodami Collectors.toSet() i Collectors.toMap()

 

Stosowanie filtrów na strumieniach

Nieco przerobimy przykład z poprzedniego podrozdziału dodając filtrowanie:

 

List<String> listaSlow = Arrays.asList("Koń","Krowa","Nietoperz","Niedźwiedź","Pies");

Stream<String> strumien = listaSlow.stream();

strumien=strumien.filter(s -> s.length()>4);

List<String> wynik = strumien.collect(Collectors.toList());

for(String s: wynik)System.out.println(s);

Kluczowa jest linia:

 

strumien=strumien.filter(s -> s.length()>4);

Metoda filter interfejsu Stream przyjmuje warunki filtracji. W tym przykładzie określiłem że interesują mnie wyłącznie wyrazy długości co najmniej 5 znaków. Filtr został zapisany przy użyciu wyrażenia lambda :

 

s -> s.length()>4

gdzie s jest parametrem wejściowych (czyli każdy element listy) , znacznik -> informuje kompilator że jest to wyrażenie lambda, a wyrażenie s.length()>4 jest właściwym filtrem. Wyrażenie filtra takie jak s.length()>4 może być dowolne, byle zwracało true albo false. W zależności od zwracanej wartości elementy są przepuszczane dalej albo eliminowane. Inny przykład filtra:

 

strumien=strumien.filter(s->s.contains("o"));

spowoduje że w efekcie otrzymam tylko elementy zawierające literę "o". Efekt działania wymienionego kodu z pierwszą wersją filtra (5 lub więcej znaków):

Efekt działania z drugą wersją filtra (zawierające literę o):

Jeśli ktoś nie lubi lub nie zna wyrażeń lambda, to analogiczny efekt można osiągnąć implementując metodę test interfejsu Predicate:

 

List<String> listaSlow = Arrays.asList("Koń","Krowa","Nietoperz","Niedźwiedź","Pies");

Stream<String> strumien = listaSlow.stream();

strumien=strumien.filter(new Predicate<String>() {

@Override

public boolean test(String s) {

return s.length()>4;

}

});

List<String> wynik = strumien.collect(Collectors.toList());

for(String s: wynik)System.out.println(s);

Co kto lubi, jak komu wygodnie...

Możesz również zastosować wiele filtrów następujących po sobie:

 

List<String> listaSlow = Arrays.asList("Koń", "Krowa", "Nietoperz", "Niedźwiedź", "Pies");

Stream<String> strumien = listaSlow.stream();

strumien = strumien.filter(s -> s.length() > 4);

strumien = strumien.filter(s -> s.contains("o"));

List<String> wynik = strumien.collect(Collectors.toList());

for (String s : wynik) {

System.out.println(s);

}

Możesz też zastosować tak zwany chaining i w jednej linii umieścić zarówno filtry jak i rzutowanie spowrotem do kolekcji:

 

List<String> listaSlow = Arrays.asList("Koń", "Krowa", "Nietoperz", "Niedźwiedź", "Pies");

Stream<String> strumien = listaSlow.stream();

List<String> wynik = strumien.filter(s -> s.length() > 4).filter(s -> s.contains("o")).collect(Collectors.toList());

Obiektu strumienia nie musisz też tworzyć osobno, możesz skrócić zapis do takiej postaci:

 

List<String> listaSlow = Arrays.asList("Koń", "Krowa", "Nietoperz", "Niedźwiedź", "Pies");

List<String> wynik =

listaSlow.stream().filter(s -> s.length() > 4).filter(s -> s.contains("o")).collect(Collectors.toList());

 

Stosowanie filtrów na obiektach złożonych

W strumieniach można przetwarzać również dane znacznie bardziej złożone niż lista elementów tekstowych. Poniżej przedstawiam przykład z użyciem klasy posiadającej trzy pola. Zaczynam od stworzenia klasy Samochod której to obiekty zamierzam przetwarzać w ramach strumieni:

 

public class Samochod {

String marka;

String model;

String numerRejestracyjny;

@Override

public String toString() {

return "Samochod{" + "marka=" + marka + ", model=" + model + ", numerRejestracyjny=" + numerRejestracyjny + '}';

}

public Samochod(String marka, String model, String numerRejestracyjny) {

this.marka = marka;

this.model = model;

this.numerRejestracyjny = numerRejestracyjny;

}

}

W innej klasie, w miejscu gdzie zamierzam zabawić się strumieniami deklaruję sobie listę elementów klasy Samochod:

 

List<Samochod> fury = Arrays.asList(

new Samochod("BMW","e46", "WWL 12345"),

new Samochod("BMW","e90", "DOL 09876"),

new Samochod("Audi","A4", "WPR 12345"),

new Samochod("BMW","e46", "WA 56788"),

new Samochod("Skoda","Octavia", "WWL 98765")

);

Chciałbym odfiltrować z tego zbioru tylko samochody marki BMW. W jaki sposób odwołać się w filtrach do pól obiektów klasy Samochod? Choćby w ten sposób:

 

List<Samochod> odfiltrowane =

fury.stream().filter(s->s.marka=="BMW").collect(Collectors.toList());

Tu również jak w poprzednim przykładzie zastosowałem wyrażenie lambda w filtrze. Do pól obiektów filtrowanych możemy się odwoływać tak jak zawsze, musimy tylko pamiętać by wyrażenie w filtrze zawsze zwracało wartość boolean.

A co jeśli pola w klasie samochód byłyby prywatne i nie można byłoby się do nich bezpośrednio odwoływać? O ile zastosowaliśmy enkapsulację i mamy gettery i settery pól, nasze odwołanie może z nich skorzystać :

 

List<Samochod> odfiltrowane =

fury.stream().filter(s->s.getMarka()=="BMW").collect(Collectors.toList());

 

Cały kod do testów:

 

List<Samochod> fury = Arrays.asList(

new Samochod("BMW","e46", "WWL 12345"),

new Samochod("BMW","e90", "DOL 09876"),

new Samochod("Audi","A4", "WPR 12345"),

new Samochod("BMW","e46", "WA 56788"),

new Samochod("Skoda","Octavia", "WWL 98765")

);

List<Samochod> odfiltrowane =

fury.stream().filter(s->s.getMarka()=="BMW").collect(Collectors.toList());

for(Samochod s:odfiltrowane)System.out.println(s);

 

Eliminacja duplikatów w strumieniach

Aby usunąć duplikaty podczas przetwarzania strumienia możemy wykorzystać metodę distinct() interfejsu Stream:

 

List<String> listaSlow = Arrays.asList("Koń", "Krowa", "Koń", "Krowa", "Pies");

List<String> przetworzone = listaSlow.stream().distinct().collect(Collectors.toList());

for(String s: przetworzone)System.out.println(s);

Po uruchomieniu:

 

Modyfikacja danych w locie

Do modyfikacji danych w locie służy funkcja map:

 

List<String> listaSlow = Arrays.asList("Koń", "Krowa", "Hipopotam", "Nietoperek", "Pies");

List<String> przetworzone =

listaSlow.stream().map(s->s.toUpperCase()).collect(Collectors.toList());

for (String s : przetworzone) {

System.out.println(s);

}

W naszym przypadku powiększa ona każdy ciąg tekstowy w strumieniu przy użyciu standardowej metody klasy String:

 

s->s.toUpperCase()

może tutaj się znaleźć dowolna metoda, w tym jakaś oprogramowana przez Ciebie.

Efekt działania:

 

Sortowanie danych z użyciem strumieni

W interfejs Stream mamy wbudowaną funkcję sorted którą można wykorzystać do posortowania elementów w strumieniu:

 

List<String> listaSlow = Arrays.asList("Koń","Zebra", "Hipopotam", "Nietoperek", "Pies");

List<String> przetworzone = listaSlow.stream().sorted().collect(Collectors.toList());

for(String s: przetworzone)System.out.println(s);

Efekt:

Domyślnie metoda sorted sortuje rosnąco, ale możemy posłużyć się metodą reverse klasy Collections by po sortowaniu rosnącym odwrócić kolekcję i w efekcie uzyskać dane posortowane malejąco:

 

List<String> listaSlow = Arrays.asList("Koń","Zebra", "Hipopotam", "Nietoperek", "Pies");

List<String> przetworzone = listaSlow.stream().sorted().collect(Collectors.toList());

Collections.reverse(przetworzone);

for(String s: przetworzone)System.out.println(s);

Efekt:

 

Ograniczanie liczby elementów w wyniku

Do ograniczenia ilości elementów w wyniku stosuje się metodę limit:

 

List<String> listaSlow = Arrays.asList("Koń","Zebra", "Hipopotam", "Nietoperek", "Pies");

List<String> przetworzone = listaSlow.stream().limit(2).collect(Collectors.toList());

for(String s :przetworzone)System.out.println(s);

W efekcie działania tej metody na strumieniu, otrzymałem dwa pierwsze elementy:

Można także łączyć limit z innymi metodami. Przykładowo chciałbym odfiltrować tylko elementy zawierające literę o, a następnie wybrać tylko 2 pierwsze elementy i powiększyć litery w wybranych elementach:

 

List<String> listaSlow = Arrays.asList("Koń","Zebra", "Hipopotam", "Nietoperek", "Pies");

List<String> przetworzone =

listaSlow.stream().limit(2).map(c->c.toUpperCase()).collect(Collectors.toList());

for(String s :przetworzone)System.out.println(s);

Pamiętaj jednak o właściwej kolejności wywoływania metod. Posortowanie w pierwszej kolejności a następnie ograniczenie ilości wierszy da inny wynik niż najpierw ograniczenie ilości wierszy a dopiero później ich sortowanie. Najpierw sortowanie a później ograniczanie liczby elementów:

 

listaSlow.stream().sorted().limit(2).map(c->c.toUpperCase()).collect(Collectors.toList());

daje taki efekt:

Napierw ograniczenie liczby elementów a następnie posortowanie daje taki efekt:

 

listaSlow.stream().limit(2).sorted().map(c->c.toUpperCase()).collect(Collectors.toList());

Istnieje też możliwość ograniczania liczby wierszy "od drugiej strony" tj pominięcie – przeskoczenie części wierszy. Używamy do tego metody skip:

 

List<String> listaSlow = Arrays.asList("Koń","Zebra", "Hipopotam", "Nietoperek", "Pies");

List<String> przetworzone =

listaSlow.stream().skip(2).sorted().map(c->c.toUpperCase()).collect(Collectors.toList());

for(String s :przetworzone)System.out.println(s);

Efekt:

 

Łączenie strumieni

Możesz połączyć ze sobą dwa lub więcej strumieni stosując metodę concat:

 

List<String> listaSlow = Arrays.asList("Koń","Zebra", "Hipopotam", "Nietoperek", "Pies");

List<String> listaSlow2 = Arrays.asList("Muchomor","Gołąbek", "Podgrzybek", "Maślak");

Stream<String> polaczone = Stream.concat(listaSlow.stream(), listaSlow2.stream());

List<String> polaczoneLista=polaczone.collect(Collectors.toList());

for(String s : polaczoneLista)System.out.println(s);

Gdybyś zechciał połączyć więcej niż 2 strumienie, będziesz musiał rozbić to na raty:

 

List<String> listaSlow = Arrays.asList("Koń","Zebra", "Hipopotam", "Nietoperek", "Pies");

List<String> listaSlow2 = Arrays.asList("Muchomor","Gołąbek", "Podgrzybek", "Maślak");

List<String> listaSlow3 = Arrays.asList("Longer","Mapet", "Fafik", "Bożenka");

Stream<String> polaczone = Stream.concat(listaSlow.stream(), listaSlow2.stream());

polaczone=Stream.concat(polaczone, listaSlow3.stream());

List<String> polaczoneLista=polaczone.collect(Collectors.toList());

for(String s : polaczoneLista)System.out.println(s);

 

Metoda peek

Metoda użyteczna zwłaszcza przy debuggowaniu. Pozwala wykonać jakąś czynność dla każdego elementu w strumieniu. W poniższym przykładzie jest to prostu wyświetlenie elementu na konsoli:

 

List<String> listaSlow = Arrays.asList("Koń","Zebra", "Hipopotam", "Nietoperek", "Pies");

List<String> listaSlow2 = Arrays.asList("Muchomor","Gołąbek", "Podgrzybek", "Maślak");

List<String> listaSlow3 = Arrays.asList("Longer","Mapet", "Fafik", "Bożenka");

Stream<String> polaczone = Stream.concat(listaSlow.stream(), listaSlow2.stream());

polaczone=Stream.concat(polaczone, listaSlow3.stream());

List<String> polaczoneLista=

polaczone.peek(s->System.out.println("Element w strumieniu:" +s)).collect(Collectors.toList());

 

Typ Optional i redukcje (count,min,max itp)

Redukcje to metody takie jak count,min,max. Operacje redukcji są operacjami kończąćymi. Redukcje zwracają jednak typ Optional, dlatego to od niego właśnie zacznę.

Typ Optional

Typ Optional opakowuje dane w strumieniach. Jest odpowiednikiem bazodanowego NVL – jeśli zwracana wartość będzie nullem, dostaniemy w zamian coś innego – alternatywnego co wprowadzimy. Prosty przykład:

 

List<String> listaSlow = Arrays.asList("Koń","Zebra", "Hipopotam", "Nietoperek", "Pies");

Optional<String> pierwszyZawierajacyMaleE

= listaSlow.stream().filter(c->c.contains("e")).findFirst();

System.out.println(pierwszyZawierajacyMaleE.orElse("nie ma..."));

Wykorzystałem tutaj metodę findFirst zwracającą pierwszy element – zwykle spełniający jakieś warunki określone w metodzie filter. W tym przypadku oczekuje zwrócenia mi pierwszego elementu mającego literę "e" w treści. Sposobowi wykorzystania Optional możemy przyjrzeć się na końcu w momencie jego wyświetlenia. Użyłem tam wbudowanej w typ Optional metody orElse która w przypadku gdyby nie znaleziono elementu spełniającego założone warunki zwróci mi wartość podaną przez parametr. Ponieważ w naszym strumieniu znalazł się element zawierający literę "e" wynik działania programu wygląda tak:

W przypadku lekkiej przeróbki metody filtrującej w ten sposób:

 

= listaSlow.stream().filter(c->c.contains("x")).findFirst();

z powodu nie znalezienia elementu zawierającego "x" dostaję:

Gdybym zaś nie użył "orElse" i próbował wyświetlić typ Optional w ten sposób:

 

System.out.println(pierwszyZawierajacyMaleE);

Dostaję taki efekt:

 

Metoda orElseGet

Działać działa, ale raczej nie jest to zbyt eleganckie. W tym przypadku podałem wartość alternatywną "z palca", jednak potencjalnie może byc potrzebne jej wyliczenie lub pobranie z bazy. W takim przypadku stosujemy metodę orElseGet która przyjmuje wyrażenia lambda:

 

private static String wartoscDomyslna(){

return "pusto";

}

 

public static void main(String[] args) {

List<String> listaSlow = Arrays.asList("Koń","Zebra", "Hipopotam", "Nietoperek", "Pies");

Optional<String> pierwszyZawierajacyMaleE

= listaSlow.stream().filter(c->c.contains("x")).findFirst();

System.out.println(pierwszyZawierajacyMaleE.orElseGet(()->wartoscDomyslna()));

}

Ograniczyłem się do prostej metody zwracającej ciąg tekstowy, ale może to oczywiście być również dużo bardziej złożone.

 

Metoda ifPresent

Inną ciekawą metodą typu Optional jest ifPresent(). Pozwala ona wykonać operację na obiekcie jeśli wartość w typie Optional istnieje. Jeśli nie istnieje, nic się nie dzieje. Przekazujemy do niej operację do wykonania np przy użyciu wyrażenia lambda.

 

List<String> listaSlow = Arrays.asList("Koń", "Zebra", "Hipopotam", "Nietoperek", "Pies");

Optional<String> pierwszyZawierajacyMaleE

= listaSlow.stream().filter(c -> c.contains("e")).findFirst();

pierwszyZawierajacyMaleE.ifPresent( c-> System.out.println(c) );

 

Tworzenie wartości Optional

Dotyczas przetwarzaliśmy otrzymany obiekt Optional, a co jeśli zechcemy stworzyć własny? Poniżej przykład metody która taki obiekt zwraca :

 

private static Optional pierwiastek(int x){

if (x!=0) return Optional.of(Math.sqrt(x));

else return Optional.empty();

}

i jej wykorzystanie:

 

public static void main(String[] args) {

System.out.println("pierwiastek z 9: "+pierwiastek(9));

System.out.println("pierwiastek z 0: "+pierwiastek(0));

}

Efekt:

 

Metody max i min i wykorzystanie interfejsu Comparator

Metody max i min pozwalają wyciągnać najwyższą lub najniższą wartość ze strumienia. Obie przyjmują jako parametr obiekt klasy implementującej interfejs Comparator. To w klasie implementującej ten interfejs oprogramowujemy sposób porównywania. Bazując na prostym przykładzie:

 

List<String> listaSlow = Arrays.asList("Koń", "Zebra", "Hipopotam", "Nietoperek", "Pies");

Mamy zadaną listę słów. Chcemy wyciągnąć ostatnią alfabetyczne wartość. Tworzymy więc klasę PolownywarkaAlfabetyczna implementującą interfejs Comparator w ten sposób:

 

public class PolownywarkaAlfabetyczna implements Comparator<String> {

@Override

public int compare(String o1, String o2) {

return o1.compareTo(o2);

}

}

Tutaj jest to najprostsze porównanie alfabetyczne, za chwilę zajmiemy się bardziej złożonymi przykładami. Wykorzystałem tutaj wbudowaną w Stringa metodę compareTo do porównania wartości. Wykorzystanie naszej nowej funkcjonalności oddelegowałem do osobnej metody z powodów strukturalnych:

 

public void odpalTo() {

List<String> listaSlow = Arrays.asList("Koń", "Zebra", "Hipopotam", "Nietoperek", "Pies");

Optional ostatni = listaSlow.stream().max(new PolownywarkaAlfabetyczna());

Optional pierwszy = listaSlow.stream().min(new PolownywarkaAlfabetyczna());

System.out.println("ostatni: "+ostatni);

System.out.println("pierwszy: "+pierwszy);

}

Jest oczywiście element uruchamiający ten bałagan:

 

public static void main(String[] args) {

Strumienie s = new Strumienie();

s.odpalTo();

}

Efekt działania:

Bazując na tym przykładzie możemy zrobić np porównywarkę długości elementów i z użyciem metod min czy max wyciągać najdłuższy bądź najkrótszy element:

 

public class PorownywarkaDlugosci implements Comparator<String> {

@Override

public int compare(String o1, String o2) {

Integer l1 = new Integer(o1.length());

Integer l2 = new Integer(o2.length());

return l1.compareTo(l2);

}

}

public void odpalTo() {

List<String> listaSlow = Arrays.asList("Koń", "Zebra", "Hipopotam", "Nietoperek", "Pies");

Optional ostatni = listaSlow.stream().max(new PorownywarkaDlugosci());

Optional pierwszy = listaSlow.stream().min(new PorownywarkaDlugosci());

System.out.println("ostatni: " + ostatni);

System.out.println("pierwszy: " + pierwszy);

}

Efekt:

Dotychczas wykorzystywaliśmy min i max na prostych ciągach tekstowych. W analogiczny sposób możemy je wykorzystywać na złożonych obiektach, różnica będzie tkwiła jedynie w implementacji Comparatora:

 

public class PorownywarkaAlfabetycznaTablic implements Comparator<Samochod> {

@Override

public int compare(Samochod o1, Samochod o2) {

return o1.getNumerRejestracyjny().compareTo(o2.getNumerRejestracyjny());

}

}

public void odpalTo() {

List<Samochod> fury = Arrays.asList(

new Samochod("BMW", "e46", "WWL 12345"),

new Samochod("BMW", "e90", "DOL 09876"),

new Samochod("Audi", "A4", "WPR 12345"),

new Samochod("BMW", "e46", "WA 56788"),

new Samochod("Skoda", "Octavia", "WWL 98765")

);

Optional pierwszy = fury.stream().min(new PorownywarkaAlfabetycznaTablic());

System.out.println("pierwszy="+pierwszy);

Optional ostatni = fury.stream().max(new PorownywarkaAlfabetycznaTablic());

System.out.println("ostatni="+ostatni);

}

public static void main(String args[]) {

Strumienie2 s = new Strumienie2();

s.odpalTo();

}

Efekt:

 

Metoda count

Metoda count zwraca ilość emementów w strumieniu. Może zwracać ilość elementów spełniających określony warunek, o ile użyjemy też metody filter:

 

List<String> listaSlow = Arrays.asList("Koń", "Zebra", "Hipopotam", "Nietoperek", "Pies");

Long iloscElementow=listaSlow.stream().filter(c->c.contains("r")).count();

System.out.println("ilość elementów z r: "+iloscElementow);

 

Metody findFirst, findAny, anyMatch,noneMatch, allMatch

Strumienie udostępniają jeszcze ciekawe metody findFirst, findAny, anyMatch,noneMatch, allMatch. Metoda findFirst zwraca pierwszy element spełniający ewentualne warunki filtracji, findAny – którykolwiek (istotnie dla wydajności przy równoległym przetwarzaniu strumieni). Metoda anyMatch sprawdza czy którykolwiek element spełnia warunek, noneMatch – czy żaden element nie spełnia warunków, a allMatch czy wszystkie spełniają:

 

List<String> listaSlow = Arrays.asList("Koń", "Zebra", "Hipopotam", "Nietoperek", "Pies");

Optional pierwszy=listaSlow.stream().filter(c->c.contains("r")).findFirst();

System.out.println("pierwszy: "+pierwszy);

Optional jakikolwiek=listaSlow.stream().filter(c->c.contains("r")).findAny();

System.out.println("jakikolwiek: "+jakikolwiek);

boolean czyKtorysMaE = listaSlow.stream().anyMatch(c->c.contains("e"));

System.out.println("czyKtorysMaE: "+czyKtorysMaE);

boolean czyWszystkieMajaE = listaSlow.stream().allMatch(c->c.contains("e"));

System.out.println("czyWszystkieMajaE: "+czyWszystkieMajaE);

boolean czyPrawdaZeZadenNieMaE = listaSlow.stream().noneMatch(c->c.contains("e"));

System.out.println("czyPrawdaZeZadenNieMaE: "+czyPrawdaZeZadenNieMaE);

 

Grupowanie i partycjonowanie

Strumienie pozwalają też dzielić elementy na grupy względem jakiejś wspólnej własności. Poniżej porzedstawiam taki przykładowy kod.

 

List<Samochod> fury = Arrays.asList(

new Samochod("BMW", "e46", "WWL 12345"),

new Samochod("BMW", "e90", "DOL 09876"),

new Samochod("Audi", "A4", "WPR 12345"),

new Samochod("BMW", "e46", "WA 56788"),

new Samochod("Skoda", "Octavia", "WWL 98765")

);

Map<String, List<Samochod>> pogrupowane = fury.stream().collect(Collectors.groupingBy(Samochod::getMarka));

for (Map.Entry<String, List<Samochod>> entry : pogrupowane.entrySet()) {

List<Samochod> grupa=entry.getValue();

System.out.println(entry.getKey() + " elementów: " +grupa.size());

}

Kluczowy jest fragment:

 

fury.stream().collect(Collectors.groupingBy(Samochod::getMarka));

a jeszcze precyzyjniej:

 

Collectors.groupingBy(Samochod::getMarka)

Element Samochod::getMarka wskazuje sposób grupowania. Grupy będą oparte o wspólne wartości zwracane przez metodę getMarka dla każdego elementu (klasy Samochod oczywiście). Wynik:

 

Kolektory strumieniowe

Kolektory strumieniowe pozwalają wykonywać operacje na utworzonych wcześniej grupach takie jak – wybieranie najwyższej i najniższej wartości w grupie, sumowanie wartości z grupy, zliczanie elementów w grupach.

 

Metoda counting

Counting wylicza dla każdej grupy ilość elementów w grupie. Na potrzeby tego i kolejnych przykładów nieco przerobiłem klasę Samochod, dodając do niej jedno dodatkowe pole liczbowe – przebieg. Dodałem też kolejny konstruktor uwzględniający to pole. Poniżej przeróbka kodu z poprzedniego przykładu. Różnica polega na tym że nie liczę ilości elementów ręcznie a używam metody counting:

 

List<Samochod> fury = Arrays.asList(

new Samochod("BMW", "e46", "WWL 12345", 120000),

new Samochod("BMW", "e90", "DOL 09876", 340000),

new Samochod("Audi", "A4", "WPR 12345", 550000),

new Samochod("BMW", "e46", "WA 56788", 199999),

new Samochod("Skoda", "Octavia", "WWL 98765", 123456)

);

Map<String, Long> pogrupowane = fury.stream().collect(Collectors.groupingBy(Samochod::getMarka, Collectors.counting()));

for (Map.Entry<String, Long> entry : pogrupowane.entrySet()) {

String marka = entry.getKey();

Long ilosc = entry.getValue();

System.out.println(marka + " elementów: " + ilosc);

}

W tym przypadku również zwracana jest mapa, natomiast wartością klucza nie jest lista samochodów, a ich liczba w każdej grupie.

Z tego powodu nasza mapa z konstrukcji <String, List<Samochod>> została zmieniona na

<String, Long>. Efekt ten sam:

 

Metody summingInt, summingLong, summingDouble

Wszystkie trzy matody służą do sumowania wartości w grupach. Różnica dotyczy tylko typu danych sumowanego pola. Poniżej mamy nieco przerobiony kod z poprzedniego przykładu:

 

List<Samochod> fury = Arrays.asList(

new Samochod("BMW", "e46", "WWL 12345", 120000),

new Samochod("BMW", "e90", "DOL 09876", 340000),

new Samochod("Audi", "A4", "WPR 12345", 550000),

new Samochod("BMW", "e46", "WA 56788", 199999),

new Samochod("Skoda", "Octavia", "WWL 98765", 123456)

);

Map<String, Integer> pogrupowane = fury.stream().collect(Collectors.groupingBy(Samochod::getMarka, Collectors.summingInt(Samochod::getPrzebieg)));

for (Map.Entry<String, Integer> entry : pogrupowane.entrySet()) {

String marka = entry.getKey();

Integer ilosc = entry.getValue();

System.out.println(marka + " suma przebiegu: " + ilosc);

}

Zmiana sprowadza się do zmiany poniższego elementu (no i oczywiście typów danych w mapach):

 

collect(Collectors.groupingBy(Samochod::getMarka, Collectors.summingInt(Samochod::getPrzebieg)));

Fragment groupingBy(Samochod::getMarka, podobnie jak poprzednio zwraca pole po którym elementy mają być grupowane. Pojawił się za to drugi parametr metody groupingBy – w zasadzie został zmieniony. Wcześniej było to:

 

Collectors.counting()

teraz jest to :

 

Collectors.summingInt(Samochod::getPrzebieg)

parametrem metody summingInt jest wywołanie metody z której zwracany jest przebieg sumowany następnie dla grup.

Efekt działania:

 

Metody maxBy i minBy

Cały czas pracujemy na tej samej liście samochodów. Tym razem jednak wyświetlimy samochody posiadające najwyższe przebiegi w grupie.

 

Map<String, Optional<Samochod>> pogrupowane

= fury.stream().collect( Collectors.groupingBy( Samochod::getMarka,Collectors.maxBy( Comparator.comparing(Samochod::getPrzebieg) ) ) );

for (Map.Entry<String, Optional<Samochod>> entry : pogrupowane.entrySet()) {

String marka = entry.getKey();

Optional<Samochod> s = entry.getValue();

System.out.println(marka + " samochod o najwyższym przebiegu: " + s);

}

Do fragmentu:

 

fury.stream().collect( Collectors.groupingBy( Samochod::getMarka,

Wszystko powinno być jasne, już to przerabialiśmy. Zwróć tylko uwagę na to że kształt mapy wygląda tak:

 

Map<String, Optional<Samochod>>

Czyli dla każdej grupy mamy zwracany obiekt Optional. Można byłoby intuicyjnie spodziewać się, że pojawi się tutaj wartość najwyższego przebiegu, a tymczasem zwracany bedzie samochód mający najwyższy przebieg. Jeśli chciałbyś uzyskać najwyższy przebieg a nie samochód, użyj metody max zamiast maxBy.

Najważniejszy fragment to:

 

Collectors.maxBy( Comparator.comparing(Samochod::getPrzebieg) )

Metoda maxBy przyjmuje przez parametr Comparator a nie metodę zwracającą wartości (jak można byłoby się spodziewać)- ale możemy odwołać się w nim do odpowiedniej metody, lub po prostu stworzyć własną implementację Comparatora.

 

Strumienie równoległe

Strumienie równoległe przyspieszają wykonywanie operacji na dużych zbiorach. Domyślnie zbiory są przetwarzane sekwencyjnie, czyli jeśli zastosujesz kilka filtrów , groupowań etc to wszystkie operacje będą wykonywane jedno po drugim. To wynika rzutowania na strumień w taki sposób:

 

nazwaListy.stream()

Zamiast tego możemy zastosować notację:

 

nazwaListy.parallelStream()

aby zrównoleglić operacje.

 

Kod źródłowy dla wszystkich przykładów:

jsystems.pl/static/download/blog/java/Strumienie.zip

 

Ten artykuł jest elementem poniższych kursów: