Ak by ste sa pýtali, aké sú najväčšie novinky v Jave 8, tak odpoveď bude jednoznačne, že Lambda výrazy (referencie na metódy) a Streamy. O tom prvom som už písal tu a tu. O tom druhom si povieme teraz. Streamy sú nový spôsob ako pracovať s množinami údajov. A podobne ako Lambda výrazy, aj oni tak trochu posúvajú Javu do sveta funkcionálnych jazykov.
Streamy predstavujú nový prístup k spracovaniu údajov. Na prvý pohľad by sa mohlo zdať, že je to len ďalšia forma kolekcií, ale ich implementácia je zásadne iná a vďaka tomu majú niekoľko výrazne iných vlastností. Streamy sa dajú charakterizovať troma vlastnosťami:
- deklaratívnosť – práca s nimi pripomína viac deklaratívne jazyky (napr. SQL), kde hovoríte kódom, čo chcete dostať a nie ako to dosiahnuť
- komponovateľnosť – spracovanie streamu (ako si o chvíľu ukážeme) je séria operácií, ktoré sa dajú jednoducho radiť jedna k druhej
- paralelizmus – od sekvenčného spracovania streamu k paralelnému sa dá dostať veľmi jednoducho. Streamy sú budované tak, aby paralelizmus bol úplne transparentný.
Aby tej teórie nebolo príliš, poďme si ukázať jednoduchý príklad. Máme zoznam mien, z ktorého chceme získať dĺžky jednotlivých položiek a pre tie, čo majú viac ako 5 znakov, vypísať ich dĺžku:
List<String> names = new ArrayList<>(); names.add("Lubomir"); names.add("Ivana"); names.add("Jan"); names.add("Ludmila"); List<Integer> filterNames = names.stream() .map(String::length) .filter(nameLength -> nameLength > 5) .collect(Collectors.toList()); filterNames.forEach(value -> System.out.println(value));
Prvých päť riadkov je klasika. Len definujeme pole a napĺňame ho údajmi. Zaujímavé to začne byť až volaním names.stream(). To je moment, kedy sa zo sveta klasických kolekcií dostávame do sveta streamov. Výsledkom tohto volania je stream, ktorý následne necháme spracovať sériou operácií. Najprv reťazce prekonvertujeme na ich dĺžky a následne necháme cez filter prejsť len tie, ktoré majú viac ako 5 znakov. Na záver z položiek, ktoré ostali v streame, zostavíme nový zoznam. Ten potom vypíšeme do konzoly.
Toto je krátka ukážka toho, ako sa dá so streamami pracovať. Poďme ale pekne po poriadku. Ako vieme stream vytvoriť?
- zo zoznamu hodnôt: Stream.of(„Jan“, „Frantisek“, „Martin“);
- z poľa: Arrays.Stream(pole);
- zo súboru: Files.lines(cesta_k_suboru);
- generovaním: Stream.generate(Math::random);
- iteratívnym volaním metódy: Stream.iterate(0, n -> n+2);
Stream máme vytvorený, čo sa s ním dá robiť? Operácie, ktoré stream podporuje, sa delia do dvoch kategórií:
- prechodné (alebo tranzitné) – vracajú opäť objekt stream, takže je k ním možné pripojiť ďalšie operácie streamu
- konečné – vracajú iný objekt ako stream, takže ukončujú spracovanie streamu
V našom príklade boli metódy map a filter prechodné metódy a metóda collect konečná. Takto vyzerá zoznam prechodných funkcií:
- filter – v streame ďalej pokračujú len objekty, ktoré splnia predikát
- distinct – v streme budú už len jedinečné hodnoty
- limit(n) – v streame ostane len prvých n prvkov
- skip(n) – prvých n prvkov je zo streamu vylúčených
- map – vykoná nejakú operáciu nad každým prvkom. Výsledok tej operácie môže byť aj prvok iného typu.
- flatMap – to isté ako predchádzajúci prípad, ale ak je výsledok operácie pole, tak nedostanem stream polí, ale stream prvkov (namiesto streamu polí len stream prvok všetkých polí)
- sorted – usporiadanie streamu (stream štandardne zachováva poradie od vytvorenia až po koniec spracovania)
A takto vyzerá zoznam konečných metód:
- forEach – vykoná operácie nad každým prvkom (návratovou hodnotou je void)
- collect – zo streamu vytvorí kolekciu podľa zadaného kolektora
- allMatch – vráti true, ak všetky prvky v streame spĺňajú podmienku
- noneMatch – vráti true, ak ani jeden prvok v streame nespĺňa podmienku
- anyMatch – vráti true, ak aspoň jeden prvok v streame spĺňa podmienku
- findAny – vráti náhodný prvok zo streamu vo forme Optional objektu
- findFirst – vráti prvý prvok zo streamu vo forme Optional objektu
- count – vráti počet prvok v streame
- reduce – zredukuje stream na jednu hodnotu podľa zadanej operácie
Variabilita práce so streamami je naozaj veľká. Stačí kombinovať rôzne prechodné funkcie a ukončiť správnou konečnou funkciou. Napr. konečná metóda reduce je celkom zaujímavá, pretože zredukuje pole do jednej hodnoty podľa nejakej operácie. Stačí jej teda dodať operáciu, ktoré z dvoch hodnôt robí jednu a ona vám zo streamu vyrobí nakoniec len jednu hodnotu (spolu s operáciou map tvoria známe duo z map-reduce algoritmu). Napr. podľa operácií Math.max alebo Math.min:
Optional<Integer> minValue = list.stream().reduce(Math::max); Optional<Integer> maxValue = list.stream().reduce(Math::min);
Toto všetko je len úvod do oblasti streamov. Nespomenul som, že existujú aj typové streamy: IntStream, DoubleStream a LongStream. Tieto nie len šetria energiu a čas tým, že nerobia boxing a unboxing nad prvkami, ale poskytujú tiež operácie relevantné daným typom. Nespomenul som tiež, že sa dá implementovať vlastný Collector (trieda, ktorú potrebuje collect metóda), alebo že sa prvky v streame dajú zoskupovať na základe definovanej operácie. A tiež som nespomenul, že medzi obyčajným streamom a paralélnym je len takýto malý rozdiel vo vytvorení:
Optional<Integer> max = list.parallelStream().reduce(Math::max);
Nevolám teda metódu stream ale parallelStream. Výsledok je, že operácie sa môžu v streame vykonávať paralelne. Ale to, či sa to nakoniec udeje, je závislé od toho, aké operácie sa so stream budú vykonávať.
Téma streamov je naozaj široká a pokojne by zaplnila niekoľko takýchto článkov. Účelom tohto bolo ukázať, že je to téma nanajvýš zaujímavá. Zásadné je uvedomiť si, že stream nie je nový druh kolekcie. Je objekt s úplne novou filozofiou, vnútornou implementáciou, a teda aj možnosťami. Je to každopádne nástroj, ktorý má určite svoje scenáre použitia, v ktorých je lepší ako čokoľvek iné.