Datovod je posloupnost prvků pocházejícího ze zdroje, který podporuje agregační operace[1]. To znamená, že:

  • Stejně jako kolekce i datovody poskytují rozhraní k práci s posloupností objektu zadaného datového typu. Ovšem na rozdíl od kolekcí tyto objekty neukládá. Podstatou kolekcí jsou totiž data, zatímco účelem existence datovodů jsou operace, které provádí nad těmito daty.
  • Datovody se vytvářejí v momentě, kdy jsou potřeba, a přijímají data ke zpracování ze zdrojů, jako jsou kolekce, pole a I/O zdroje.
  • Datovody podporují operace podobné těm databázovým a běžným ve funkcionálních programovacích jazycích, jako jsou filter, match, find, map atp.

Terminologie editovat

Anglický termín stream je možné přeložit jako proud. Tento termín se již ovšem používá pro jiný druh objektů – objektů sloužících k dopravě dat od zdroje k cíli (datové proudy). Aby se předešlo případným zmatkům v terminologii, v tomto článku se pracuje s termínem datovod.

Důvody zanesení do jazyka editovat

Autoři osmé verze programovacího jazyka Java[2] se rozhodli vytvořit nový typ objektů (datovody), které umožňují pracovat se skupinou jiných objektů. Datovody jsou v určitém smyslu ekvivalentem kolekcí (seznam, mapa, set), jsou ovšem mnohem sofistikovanější. Kolekce se využívají ve valné většině všech Java programů, protože programy musí nějakým způsobem umět seskupovat a zpracovávat data. Ačkoliv jsou tedy kolekce pro programy nezbytné, jejich dosavadní možnosti jsou omezené a neodpovídají v současnosti potřebám programátorů.

Příkladem může být situace, kdy je potřeba vytvořit kolekci bankovních transakcí klienta k tomu, aby se tyto transakce mohly dále zpracovat a získat z nich další data jako například největší částku peněz, kterou klient jednorázově utratil. Tyto operace prováděné nad kolekcemi jsou velmi podobné SQL dotazům a většina databází je umožňuje provádět deklarativně. Psáno pomocí SQL dotazu, největší částku peněz utracenou jednorázově a identifikační číslo této transakce lze získat jako:

Ukázka SQL dotazu na nalezení největší jednorázově utracené částky

 SELECT id, MAX(value) FROM transactions;

Programátor pouze určí, co očekává za výsledek a SŘBD udělá všechnu práci za něj. Díky tomu se může více věnovat definici výsledku, který potřebuje, aniž by se musel starat i o implementaci vyhledávacího algoritmu. Rovněž je často potřeba umožnit rychlé zpracování velkého množství dat v kolekcích a dosavadní paralelní programování je náročné a velmi náchylné na chyby. Datovody umožňují zpracovávat data deklarativním způsobem a zároveň mají možnost využít více procesorů k jejich práci, aniž by musel programátor vytvářet jakákoli vlákna. Potřebu modernizovat kolekce si programátoři uvědomovali už dlouho, proto se někteří z nich rozhodli vytvořit vlastní knihovny, které by rozšířily a vylepšily jejich funkčnost. Příkladem může být knihovna Guava a Apache Commons Collections. Autoři Java 8 přichází s vlastní oficiální verzí knihovny pro efektivnější manipulování s kolekcemi dat.

Datovody versus kolekce editovat

Datovod se od kolekcí liší v několika věcech:

  • Datovod přijatá data pouze zpracuje a pošle dál. To znamená, že si je neukládá a tím nezabírá žádnou paměť.
  • Datovod nemění vstupní data. To znamená, že mohou být zpracovávána několika procesory zároveň.
  • Datovod pracuje na základě odložené vyhodnocovací strategie, čímž šetří výpočetní výkon až do poslední chvíle. Začne se provádět, až je ho třeba.
  • Datovody umožňují vytvářet proudy, protože některé z nich vracejí jako výsledek datovod, který obsahuje zpracovaná data. Toto zřetězení umožňuje využívat případné optimalizace, jako je například strategie odloženého vyhodnocení.

Druhy operací editovat

Knihovna java.util.stream obsahuje mj. rozhraní Stream<T>, který definuje operace, které se mohou vykonat nad zpracovávanými daty. Tyto operace lze rozdělit do dvou kategorií na průběžné (intermediate) a koncové (terminal).

Průběžné operace

Průběžné operace jsou charakteristické tím, že vracejí jako výslednou hodnotu datovod, což umožňuje řetězení operací nad zpracovávanými daty. Tyto operace vždy vracejí nově vytvořený datovod a navíc neprovádí žádné zpracování dat, dokud není nad datovodem zavolána koncová operace. Tato strategie vyhodnocování je označována jako „odložená“ (lazy). Níže se nachází tabulka s výčtem některých průběžných operací.

Operace Výsledná hodnota Notace lambda výrazu [3]
filter Stream<T> T -> boolean
map Stream<T> T -> R
limit Stream<T>
sorted Stream<T> (T, T) -> int

Koncové operace

Tyto operace produkují výsledek z proudu datovodů a zakončují jejich činnost. Níže uvedená tabulka obsahuje výčet několika nejpoužívanějších koncových operací. Níže se nachází tabulka s výčtem některých koncových operací.

Metoda Výsledná hodnota Účel
forEach void Na každý prvek aplikuje lambda výraz.
collect long Spočítá počet prvků v datovodu.
count Kolekci, Integer Vytvoří z proudu kolekci.

Níže uvedený kód znázorňuje zřetězení datovodů, na kterém je možné vidět průběžné a koncové operace. Cílem tohoto kódu je vyfiltrovat nanejvýš 2 studenty, kteří jsou starší 18 let, a uložit si jejich jména.

 
List<String> names = students.stream()  //průběžná op.
                             .filter(student -> student.getAge() > 18) //průběž.
                             .map(Student::getName)             //průběžná op.
                             .limit(2)                          //průběžná op.
                             .collect(Collectors.toList()); 	//koncová op.

Externí a interní iterátor editovat

Aby bylo možné procházet prvky v kolekci, musí být iterace nadefinovaná programátorem použitím například for each cyklu. Tento způsob iterace se nazývá externí. V níže uvedeném kódu je seznam studentů procházen sekvenčně pomocí for each cyklu a jména studentů jsou ukládána do seznamu names.

Ukázka kódu znázorňující procházení seznamu studentů pomocí externího iterátoru

 List<String> names = new ArrayList<>();
 for (Student student : students) {
     names.add(student.getName());
 }

Datovody ovšem umožňují tzv. interní iteraci, což znamená, že kód k procházení kolekce je nadefinovaný v samotné knihovně datovodů. Programátor tedy pouze určí, která kolekce se má procházet, a JDK následně rozhodne, jak se bude daná kolekce procházet. Níže uvedená ukázka kódu znázorňuje procházení seznamu pomocí interního iterátoru.

Ukázka kódu znázorňující procházení seznamu studentů pomocí interního iterátoru

 List<String> names = students.stream()
                              .map(Student::getName)
                              .collect(Collectors.toList());

Zdrojem vstupních dat pro datovod je seznam studentů. Metoda map poslouží k získání jmen studentů a metoda collect vrátí seznam obsahující výsledné prvky.

Příklady použití editovat

Na tomto příkladu je znázorněno procházení kolekcí a filtrování prvků pomocí datovodů. Cílem kódu je vyfiltrovat všechny zaměstnance starší 40 let.

 List<Employee> employeesAgedOverForty = employees.stream() 
                                                  .filter(e -> e.getAge() > 40) 
                                                  .collect(Collectors.toList());

Metoda stream vytváří datovod obsahující jednotlivé prvky seznamu zaměstnanců a vrací na výstupu tento datovod. Tato metoda využívá interního iterátoru k procházení jednotlivých prvků seznamu. Zdrojem dat pro metodu filter jsou prvky datovodu, který byl vytvořen v předchozí operaci. Metoda filter následně vytváří nový datovod, který obsahuje prvky splňující podmínku, která byla metodě předána jako parametr. Metoda collect nakonec vloží vyfiltrované prvky do měnitelného kontejneru. Ve výše uvedeném příkladu je použita metoda toList, která vrátí seznam obsahující výsledné prvky.

Metoda filter dostává jako parametr instanci funkčního rozhraní Predicate<T>[4] deklarující metodu test, která jako parametr dostává objekt typu T a vrací boolean. Tato metoda slouží k vyhodnocení pravdivosti predikátu nad zadanou hodnotu. V tomto příkladě je metodě filter předán lambda-výraz e -> e.getAge() > 40, který vyhodnocuje, zda je zaměstnanci více než 40 let. Jak bylo řečeno výše, metoda toList vrací seznam (java.util.List<E>) obsahující výsledné prvky agregačních operací.

Přínosy editovat

Používání API, které je nadefinované v knihovně java.util.stream[5] umožňuje programátorovi psát kód, který je stručný, přehledný a deklarativní. Tím, že vlastní implementace funkčnosti je ponechána knihovně, je umožněno vykonávání kódu hned na několika procesorech. Rychlost programu se tím zrychluje a práce programátora značně usnadňuje.

Reference editovat

  1. Agregační operace [online]. Oracle. Dostupné online. 
  2. JDK 8 [online]. OpenJDK, 2013-04-18 [cit. 2014-01-28]. Dostupné online. 
  3. Lambda expressions [online]. Oralce. 
  4. Predicate [online]. Oralce. 
  5. java.util.stream [online]. Oralce. 

Literatura editovat

  • URMA, Raoul-Gabriel. Java 8 in Action: Lambdas, Streams, and functional-style programming. Cambridge: Manning Publications, 2014. Dostupné online. ISBN 978-1617291999. 
  • PECINOVSKY,, Rudolf. Java 8 – učebnice objektové architektury pro mírně pokročilé. 3. vyd.. Praha: Grada Publishing a.s.,, 2012. ISBN 978-80-247-4638-8. 
  • WAMPLER, Dean. Functional Programming for Java Developers: Tools for Better Concurrency, Abstraction, and Agility. Cambridge: O'Reilly Media, 2011. Dostupné online. ISBN 978-1-4493-1102-5. 

Související články editovat

Externí odkazy editovat