Vysoká škola ekonomická v Praze Fakulta informatiky a statistiky Katedra informačních technologií Studijní program: Aplikovaná informatika Obor: Informační systémy a technologie Možnosti deklarativního programování v jazyce Java 8 DIPLOMOVÁ PRÁCE Student : Bc. Maxim Rytych Vedoucí : Ing. Rudolf Pecinovský, Csc. Oponent : Ing. Jarmila Pavlíčková, Ph.D. 2015
Prohlášení: Prohlašuji, že jsem diplomovou práci zpracoval samostatně a že jsem uvedl všechny použité prameny a literaturu, ze které jsem čerpal. V Praze dne 14. prosince 2015.................................. Maxim Rytych
Poděkování Na tomto místě bych rád poděkoval svým nejbližším za podporu: mojí rodině a nejbližším přátelům, bez nichž bych se ke studiu vysoké školy zřejmě vůbec neodhodlal. Dále bych chtěl poděkovat vedoucímu mé práce panu Ing. Rudolfu Pecinovskému, CSc., za velmi vstřícný přístup, trpělivost, cenné připomínky, rady a množství času, které mi při vypracovávání diplomové práce věnoval.
Abstrakt Tato práce se zabývá možnostmi deklarativního programování v nové verzi jazyka Java 8, a to konkrétně prostřednictvím prvků převzatých z domény funkcionálních programovacích jazyků v podobě funkce jako hodnoty a proudů dat využívajících odloženého vyhodnocení nazývaných v této práci termínem datovody. Cílem práce je předvést možnosti deklarativního programování prostřednictvím těchto prvků, analyzovat jejich implementaci a předvést, jak navrhnout vlastní řešení. Přínos práce spočívá zejména v ukázce možností nových prvků, rozboru jejich implementace a návrhu vlastního řešení. Výstupy může použít český čtenář, alespoň mírně pokročilý v oboru informačních technologií. Práce je rozdělena na část teoretickou a praktickou. Teoretická část je pokryta kapitolami 3-8. Teoretická část hovoří o motivaci zavedení nových prvků, popisuje funkcionální programování a jeho základní principy, poté ukazuje principy nově zavedených prvků a končí popisem balíčku java.util.stream. Praktická část je pokryta kapitolami 9 a 10. V praktické části jsou popsány operace datovodů a ukázány příklady vlastních řešení. Klíčová slova Java 8, deklarativní programování, funkcionální programování, lambda-výrazy, odkazy na metody, datovody, balíček java.util.stream, funkční interfejs, implicitní metody, interfejs Collector, interfejs Spliterator
Abstract This paper concerns itself with possibilities of declarative programming in the new version of Java 8 language, specifically using elements adopted from the domain of functional programming languages function as a value and lazy streams of data. The goal of this paper is to demonstrate possibilities of declarative programming using these elements, analyze its implementation and design own extensions. The contribution lies particularly in showing possibilities of the new elements, implementation analysis and design of a new functionality. The output can be used by a Czech reader, who is at least slightly advanced in the field of information technologies. The paper is divided into a theoretical and practical parts. Theoretical part is covered by chapters 3-8. Theoretical part describes motivation for introduction of the new elements, describes functional programming and its basic principles, then it shows basic principles of the newly introducted elements and ends with the description of the java.util.stream package. Pactical part is covered by chapters 9 and 10. Practical part concerns with stream operations and extension design of existing functionality. Keywords Java 8, declarative programming, functional programming, lambda-expressions, method references, Streams, java.util.stream package, functional interface, default methods, Collector interface, Spliterator interface
Obsah 1 Úvod 1 1.1 Vymezení tématu práce a důvod výběru tématu... 1 1.2 Cíle práce... 1 1.3 Způsob/metoda dosažení cíle... 2 1.4 Struktura práce... 2 1.5 Výstupy práce a očekávané přínosy... 2 2 Rešerše zdrojů... 3 3 Motivace změn v Javě 8... 4 4 Funkcionální programování... 7 4.1 λ-kalkul... 7 4.1.1 Redukce a konverze... 9 4.2 Charakteristické prvky funkcionálních programovacích jazyků... 10 4.2.1 Funkce jako entity první úrovně... 10 4.2.2 Funkce vyššího řádu... 10 4.2.3 Odkazová transparentnost... 11 4.2.4 Neměnnost stavů... 11 4.2.5 Odložené vyhodnocení... 11 4.2.6 Absence vedlejších efektů... 11 4.2.7 Deklarativní způsob programování... 12 5 Funkcionální prvky v Javě 8... 13 5.1 Funkce jako hodnota... 13 5.1.1 Lambda-výrazy... 13 5.2 Seznam s odloženým vyhodnocením... 14 5.2.1 Datovody... 15 6 Lambda-výrazy (a odkazy na metody)... 16 6.1 Funkční interfejs... 16 6.2 Syntaxe lambda-výrazů... 17 6.3 Odkazování na metody... 18 6.4 Od parametrizace hodnot k lambdám... 20 7 Datovody... 28 7.1 Datovody jako seznamy s odloženým vyhodnocením... 28 7.2 Řetězení operací... 30 7.3 Názornější příklad... 32
8 Balíček java.util.stream... 38 8.1 Charakteristické prvky balíčku java.util.stream... 38 8.1.1 Datovody a kolekce... 39 8.1.2 Interní a externí iterátory... 39 8.1.3 Vytváření datovodů... 40 8.1.4 Operace s datovody... 41 8.1.5 Stavové a bezestavové průběžné operace... 45 8.1.6 Omezené a neomezené stavové operace... 45 8.1.7 Paralelní zpracování... 46 8.1.8 Bezkonfliktnost... 46 8.1.9 Bezstavové chování... 48 8.1.10 Vedlejší efekty... 48 8.1.11 Pořadí... 49 8.1.12 Redukce... 50 8.1.13 Proměnná redukce... 51 8.1.14 Asociativita... 51 8.1.15 Nízkoúrovňová konstrukce datovodů... 51 8.2 Obsah balíčku java.util.stream... 52 8.2.1 Interfejs BaseStream<T,S extends BaseStream<T,S>>... 53 8.2.2 Interfejs Collector<T,A,R>... 53 8.2.3 Interfejs Stream<T>... 54 8.2.4 Interfejs Stream.Builder<T>... 54 8.2.5 Specializované interfejsy pro primitivní datové typy... 55 8.2.6 Třída Collectors... 55 8.2.7 Třída StreamSupport... 55 8.2.8 Výčtový typ Collector.Characteristics... 55 8.3 Další prvky Javy 8 související s datovody... 56 8.3.1 Interfejs Spliterator<T>... 56 8.3.2 Třída Optional<T>... 60 8.3.3 Implicitní metody... 62 9 Operace s datovody... 64 9.1 Aplikace na zápis a čtení XML souborů... 64 9.1.1 Aplikace na zápis XML souborů... 64 9.1.2 Aplikace na čtení XML souborů... 65 9.2 Jednoduché koncové operace... 65 9.2.1 Zjištění, zda existuje alespoň jeden prvek splňující danou podmínku... 65 9.2.2 Zjištění, zda všechny prvky splňují danou podmínku... 66 9.2.3 Zjištění, zda ani jeden z prvků nesplňuje danou podmínku... 67 9.2.4 Získání libovolného prvku... 67 9.2.5 Získání prvního prvku... 68
9.2.6 Vyhledání maximální a minimální hodnoty... 68 9.2.7 Počet prvků... 69 9.2.8 Provedení operace pro každý prvek... 69 9.2.9 Provedení operace pro každý prvek při respektování implicitního pořadí... 69 9.2.10 Získání prvků v podobě pole... 70 9.3 Průběžné operace... 70 9.3.1 Filtrování... 71 9.3.2 Mapování... 71 9.3.3 Řazení... 72 9.3.4 Nalezení unikátních prvků... 72 9.3.5 Omezení počtu prvků... 73 9.3.6 Přeskočení prvků... 73 9.3.7 Provedení operace pro každý prvek... 73 9.3.8 Vytvoření datovodu o jednom nebo více prvcích... 74 9.3.9 Spojení datovodů... 75 9.3.10 Vytvoření prázdného datovodu... 75 9.3.11 Vytvoření datovodu generováním... 75 9.3.12 Vytvoření datovodu iterováním... 75 9.3.13 Mapování na jednu úroveň... 76 9.4 Složité koncové operace... 77 9.4.1 Redukce... 78 9.4.2 Sběr... 80 9.5 Další metody třídy Collectors... 85 9.5.1 Metody averaging... 85 9.5.2 Metoda collectingandthen()... 86 9.5.3 Metody joining... 86 9.5.4 Metoda mapping()... 88 9.5.5 Metody summarizing... 89 9.5.6 Metody summing... 90 9.5.7 Metoda tocollection()... 90 9.5.8 Metoda toset()... 90 10 Vlastní řešení... 92 10.1 Interfejs IStreamable<T>... 92 10.2 Implementace vlastního kolektoru... 97 10.3 Implementace vlastního spliterátoru... 100 11 Závěr 106 Terminologický slovník... 107
Seznam literatury... 111
1 Úvod 1 1 Úvod V březnu roku 2014 vydala společnost Oracle novou, osmou, verzi populárního programovacího jazyka Java. Zřejmě nejvýznamnější změnou bylo zavedení prvků převzatých z domény funkcionálních programovacích jazyků proudů dat funkcionálního stylu nazývaných v této práci termínem datovody, a s nimi souvisejícího prvku umožňujícího pracovat s funkcí jako hodnotou v podobě lambda-výrazů či odkazů na metody. Množinu těchto prvků dále doplňují konstrukce jako funkční interfejs nebo implicitní metody, z nichž posledně jmenované přináší poměrně významnou změnu z pohledu návrhu architektury programů, neboť zavádí jistou možnost vícenásobného dědění implementace. Jádro datovodů se nachází v balíčku java.util.stream, ale některé fundamentální prvky jsou mimo tento balíček jako například interfejs Spliterator, který je vlastně jedním ze základních stavebních prvků datovodů. 1.1 Vymezení tématu práce a důvod výběru tématu Práce se zabývá novými možnostmi deklarativního programování s pomocí nových prvků převzatých z domény funkcionálních programovacích jazyků lambda-výrazů, odkazů na metody a datovodů. Toto téma jsem si vybral, protože mě oblast programování zajímá a je i vhodné vzhledem k mému zaměstnání vývojáře pro rozšíření znalostí. 1.2 Cíle práce Jednou z významných motivací nové verze jazyka Java bylo umožnění jednoduššího paralelního zpracování tím způsobem, že uživatel bude odstíněn od nutnosti imperativní implementace a bude mu nabídnuta mnohem pohodlnější deklarativní cesta. Ačkoli primární motivací je využití paralelního zpracování, lze datovody využívat i pro sériové zpracování. S datovody je možné pracovat v deklarativním stylu velmi podobném například databázovému dotazovacímu jazyku SQL. Hlavními cíli práce bude: 1. Podrobně popsat fungování nových prvků převzatých z domény funkcionálních programovacích jazyků konstrukce umožňující pracovat s funkcí jako hodnotou v podobě lambda-výrazů a odkazů na metody a konstrukcí známých ve funkcionálních programovacích jazycích jako seznamy s odloženým vyhodnocením v podobě datovodů. 2. Na příkladech demonstrovat použití výše uvedených konstrukcí.
1 Úvod 2 3. Ukázat, jak navrhnout vlastní řešení v případech, kdy výchozí nelze použít. 1.3 Způsob/metoda dosažení cíle Pro dosažení cíle budu používat zejména odborných zdrojů, ke kterým bude primárně patřit dokumentace společnosti Oracle vlastníka jazyka Java. Nejprve budou nastíněny základní koncepty funkcionálního programování, poté bude následovat popis adopce těchto prvků jazykem Java a na konci práce bude ukázáno, jak navrhnout a implementovat vlastní rozšíření. 1.4 Struktura práce Práce je rozdělena na část teoretickou a praktickou. Teoretická část je pokryta kapitolami 3-8. Teoretická část hovoří o motivaci zavedení nových prvků, popisuje funkcionální programování a jeho základní principy, poté ukazuje principy nově zavedených prvků a končí popisem balíčku java.util.stream. Praktická část je pokryta kapitolami 9 a 10. V praktické části jsou popsány operace datovodů a navržena vlastní řešení. 1.5 Výstupy práce a očekávané přínosy Přínos práce bude spočívat zejména v popisu nových konstrukcí a návrhu vlastních řešení. S ohledem na významně menší množství české literatury v této oblasti oproti anglické, může být tato práce přínosná zejména pro českého čtenáře, avšak čtenáře alespoň mírně pokročilého v oboru informačních technologií může jít například o studenty zajímající se o danou oblast a/nebo píšící závěrečnou práci na související téma, vývojáře nebo jiné osoby se zájmem v této oblasti.
2 Rešerše zdrojů 3 2 Rešerše zdrojů Předmětnou oblast pokrývá široké spektrum zdrojů od oficiální dokumentace po nejrůznější vývojářské weby a blogy. Naprostá většina literatury je v anglickém jazyce. Jako primární zdroj byl použit Java Language Specification: Java SE 8 Edition [1]. Jde o kompletní technickou příručku pro programovací jazyk Java. Kniha poskytuje úplný, detailní a přesný popis prvků jazyka. Nejvíce bylo čerpáno z kapitol popisujících nové prvky přidané do osmé verze, tj. lambda-výrazy, odkazy na metody, implicitní metody atd. Dalším z důležitých zdrojů použitých v této práci byla dokumentace API Javy 8 [2]. V dokumentaci nalezneme popis balíčků, tříd, interfejsů, metod atd. V řadě případů ale dokumentace poskytuje vysvětlení, které je vhodné doplnit další literaturou. Z tohoto zdroje byla nejvíce použita část popisující balíček java.util.stream. Jako třetí zdroj byla použita kniha Java 8 in Action: Lambdas, Streams and functional-style programming [3]. Jedná se o velmi srozumitelně napsaného průvodce popisujícího a vysvětlujícího nové prvky Javy 8 s důrazem na prvky převzaté z domény funkcionálních programovacích jazyků. Výhodou této knihy je, že se nazastavuje pouze na úrovni kódu, ale snaží se na celou oblast nahlížet z vyšší perspektivy. Další silnou stránkou je, že výklad průběžně ilustruje na příkladech, aby bylo pro čtenáře jednodušší získané teoretické znalosti převést na praktickou úroveň. Kniha byla velmi vhodná pro pochopení některých skutečností, které nebyly (a ze své podstaty ani být nemohly) v prvních dvou uvedených zdrojích vysvětleny. Tento zdroj posloužil pro pochopení základních principů funkcionálního programování, nových prvků funkcionálního programování adoptovaných novou verzí Javy a jako inspirace pro příklady použité v této práci. Dalším zdrojem byla publikace Rudolfa Pecinovského Java 8: Úvod do objektové architektury pro mírně pokročilé [4], která se dobře doplňovala s výše uvedenými publikacemi a byla jedním z mála použitých zdrojů v českém jazyce. Výhodou této knihy je, že nahlíží na jazyk Java z jiné perspektivy než ostatní, čímž čtenáři pomáhá uvědomit si řadu skutečností, které jiné zdroje ignorují. Poslední skupinou zdrojů byly ostatní zdroje internetu, z kterých lze jmenovat například web vývojářských profesionálů www.stackoverflow.com nabízející nepřeberné množství teoretických i praktických znalostí z oblasti informačních technologií.
3 Motivace změn v Javě 8 4 3 Motivace změn v Javě 8 Vše podléhá vývoji, programovací jazyky nevyjímaje. Hybné síly v našem případě můžeme zjednodušeně rozdělit do čtyř skupin: 1. business část společnosti 2. programátoři 3. úroveň technologie 4. světonázor Požadavky business části společnosti lze v dnešní době charakterizovat velkým objemem dat, rychlostí a kvalitou jejich zpracování, požadavky programátorů pohodlností práce s kódem, třetí skupina určuje, co je a co není možné, a poslední ovlivňuje způsob řešení všeho předchozího. V tomto modelu se prvky vzájemně systémově ovlivňují a vytváří třecí plochy, které působí na vývoj programovacích jazyků. Co bylo třecími plochami při přechodu od Javy 7 k Javě 8? Následující čtyři body odpovídají výše uvedeným. 1. Ve své bakalářské práci jsem se dotkl otázky růstu objemu dat. Mimo jiné jsem citoval zdroj [5], který toto názorně ilustroval následujícím způsobem: v roce 2011 má být vytvořeno a replikováno neuvěřitelných 1,8 zetabajtů dat. Takové množství by například vytvořili obyvatelé Spojených států, kdyby po dobu 26 976 let zveřejnili každou minutu 3 příspěvky v síti Twitter. Odpovídá to také více než 200 miliardám filmů ve vysokém rozlišení (každý o délce dvě hodiny). Zhlédnutí by jednomu člověku při nepřetržitém sledování trvalo 47 milionů let. 1,8 zetabajtů je také množství informací, které by naplnilo 57,5 miliardy tabletů Apple ipad s kapacitou 32 GB. Z nich by pak šla postavit Velká čínská ipadová zeď s dvojnásobnou průměrnou výškou oproti té původní, nebo postavit 6 metrů vysokou zeď lemující Jižní Ameriku Je tedy neustále třeba zpracovávat stále větší a větší objemy dat s tímto trendem se pojí termín Big Data. 2. Data jsou ukládána do datových struktur, prostřednictvím kterých se s nimi provádí nejrůznější operace. Dle [3] jsou nejvíce používanými datovými strukturami Javy kolekce. 3. Dle [4] roste v dnešní době výkonnost počítačů hlavně růstem počtu procesorů. 4. V oblasti programování lze sledovat jednoznačný trend, který posouvá imperativní pohled směrem k pohledu deklarativnímu. Existuje tedy požadavek zpracovávání velkého množství dat ve velké rychlosti a kvalitě. Jsou k dispozici datové struktury, které programátoři pro tento účel nejčastěji používají. Existují technologické aspekty, které vedou ke zvyšování výkonu počítačů zejména
3 Motivace změn v Javě 8 5 prostřednictvím růstu počtu procesorů. V neposlední řadě se projevuje i obecný trend ve vývoji programovacích jazyků jdoucí deklarativním směrem. Vše výše jmenované vede k následujícímu: Práce s kolekcemi v Javě 7 se stává pro takový objem dat velmi pracnou a nepohodlnou záležitostí. Výkon počítačů je zvyšován zejména růstem počtu procesorů. Je tedy nutné zamyslet se nad možností využití vícejádrové architektury použitím paralelního zpracování. Programování vícevláknových aplikací je velmi náročné, takže v této oblasti je tím spíše nutné zavést takový styl, který by programátora zcela odstínil od nutnosti rozdělit zpracování programu mezi více vláken. V neposlední řadě působí všeobecný trend, který posouvá programovací jazyky od otázky způsobu řešení k otázce předmětu řešení. Pokud měla Java zůstat nadále použitelným jazykem, bylo nutné na systémové změny zareagovat. Jednou z těchto reakcí je právě převzetí prvků funkcionálního programování. Následující kód je zapsán v Javě 7. * Metoda přijme seznam instancí typu Person - osob a vrátí nový seznam * instancí typu String - křestní jména osob mladších třiceti let. * Metoda je napsána bez pomoci datovodů. public static List<String> youngpeoplenamesold(list<person> list) { List<Person> youngpeople = new ArrayList<>(); for (Person p : list) { if (p.getage() < 30) { youngpeople.add(p); Collections.sort(youngPeople, new Comparator<Person>() { @Override public int compare(person p1, Person p2) { return Integer.compare(p1.getAge(), p2.getage()); ); List<String> youngpeoplenames = new ArrayList<>(); for (Person p : youngpeople) { youngpeoplenames.add(p.getname()); return youngpeoplenames; Tento kód je zapsán pomocí nových konstrukcí Javy 8. * Metoda přijme seznam instancí typu Person - osob a vrátí nový seznam * instancí typu String - křestní jména osob mladších třiceti let.
3 Motivace změn v Javě 8 6 * V této metodě se používají datovody. public static List<String> youngpeoplenamesadvanced(list<person> list) { List<String> youngpeoplenames = list.stream().filter(p -> p.getage() < 30).sorted(comparing(Person::getAge)).map(Person::getName).collect(Collectors.toList()); return youngpeoplenames; Kód zapsaný za použití nových konstrukcí Javy 8 je stručnější, přehlednější a jeho zápis odpovídá deklarativnímu konceptu.
4 Funkcionální programování 7 4 Funkcionální programování Funkcionální programování je specifický programovací pohled pracující s výpočty prostřednictvím matematických funkcí. Funkcionální programovací jazyky odpovídají deklarativnímu paradigmatu, tj. jsou založeny na myšlence programování aplikací pomocí definic, co se má udělat, namísto jak se to má udělat. [3] Základem funkcionálních programovacích jazyků je zápis programu ve tvaru výrazů, jejichž nejdůležitějšími složkami jsou funkce a jejich parametry. Výpočet probíhá v podobě zjednodušování výrazu až do tzv. normální formy, tj. takového stavu, kdy výraz již dále zjednodušit nelze. Tento tvar je výsledkem výpočtu. Řada funkcionálních programovacích jazyků vychází z λ-kalkulu. [6] 4.1 λ-kalkul λ-kalkul (nebo také lambda-kalkul) je systém využívaný matematikou a teoretickou informatikou ke studiu funkcí a rekurze. Byl vytvořen Alonzo Churchem ve spolupráci se Stephenem Colem Kleenem. Jeho první verze pochází z 30. let 20. století. V roce 1936 Church oddělil a publikoval samostatnou pro informatiku podstatnou část, která se nazývá netypovým lambda-kalkulem. Na přelomu 50. a 60. let 20. století začal být používán v informatice ke studiu vyčíslitelnosti a formálních jazyků. Stal se teoretickým základem řady funkcionálních programovacích jazyků. [6] Na rozdíl od matematického přístupu ke zkoumání funkcí, tj. ve smyslu přiřazování číslu x z množiny čísel (většinou reálných nebo komplexních) A právě jednoho čísla y z množiny čísel B podle konkrétního předpisu, nahlíží lambda-kalkul na funkce jako na metodu výpočtu. Lze jej chápat jako jednoduchý univerzální programovací jazyk. [6] Řecké písmeno lambda (λ) označuje parametry funkce. Church se inspiroval značením volných proměnných v díle Principia Mathematica, kde se značily symbolem ˆ (stříška, cirkumflex). Church se na základě toho rozhodl značit parametry velkým písmenem lambda Λ, ale poté přešel k malému písmenu lambda. [7] Na základě důležitosti pojmů vázaných proměnných a substituce se rozlišuje netypový a typový lambda-kalkul. Z historického hlediska je důležitější lambda-kalkul netypový, ve kterém nejsou aplikace funkcí nijak omezené. Typový lambda-kalkul je variací, která aplikace funkcí omezuje v tom smyslu, že mohou být aplikovány pouze v případě, že jsou schopny přijmout vstupní typ dat. [6] Základní prvky lambda-kalkulu se nazývají lambda-výrazy. Vyskytují se ve třech podobách: [6]
4 Funkcionální programování 8 Proměnné jak jsou definovány v matematice (x, y, ) Abstrakce definice funkce, např. funkce zapsaná matematicky ƒ(x) = x + 1, se zapíše v lambda-kalkulu jako λx. x + 1, tj. mezi λ a tečkou se nachází parametry funkce, za tečkou tělo funkce. Aplikace volání funkce, v matematice se funkce zavolá např. s parametrem 9 takto: ƒ(9), v lambda-kalkulu jsou volány takto: (λx. x + 1) 9. Lambda-výraz lze rekurzivně vyjádřit následujícím způsobem: [8] výraz = proměnná abstrakce aplikace abstrakce = λ proměnná. výraz aplikace = výraz výraz Výše bylo zmíněno, že písmeno lambda označuje parametry funkce vázanou proměnnou. Oproti tomu existují i volné proměnné. Rozdíl mezi nimi je ukázán na následujícím příkladu. Je dána funkce ƒ(x) = x + y, která se v lambda-kalkulu zapíše jako λx. x + y. Proměnná x je v tomto případě vázaná, proměnná y volná. Proměnná je tedy v lambda-výrazu vázaná, pokud je parametrem nějaké funkce. Proměnná se vždy váže na nejbližší lambdu směrem doleva. Rozlišování volných a vázaných proměnných je důležité při provádění operací s výrazy. [6] V lambda-kalkulu má funkce vždy jenom jeden parametr. Pokud nastane situace, kdy je třeba mít funkci s více parametry, využije se tzv. curryingu [3]. Do češtiny zde budu tento výraz překládat jako curryifikace. Jde o techniku, kdy je funkce o více parametrech transformována na sekvenci funkcí, kdy každá přijímá jeden parametr. Funkci ƒ(x, y) = x + y, lze přepsat na lambda-výraz λx. λy. x + y. Při aplikaci hodnot, např. 1 a 2, bude vyhodnocení probíhat následujícím způsobem: [6] (λx. λy. x + y) 1 2 -> (λx = 2. λy. x + y) 1 -> (λy. 2 + y) 1 -> λy = 1. 2 + y -> 2 + 1 -> 3.
4 Funkcionální programování 9 4.1.1 Redukce a konverze Základní operace v lambda-kalkulu jsou redukce a konverze. Jejich prostřednictvím lze lambda-výrazy upravovat. Existují tři základní operace: α-konverze (alfa), β-redukce (beta) a η-redukce (éta). [6] α-konverze umožňuje přejmenování vázaných proměnných jedná se tedy v podstatě o substituci. V tomto případě je však třeba dbát na to, aby se při úpravě výrazu nezměnila některá z volných proměnných v proměnnou vázanou. Je definován následující lambda-výraz: λx. x + 1 Tento výraz lze uplatněním alfa-konverze upravit například na Pokud ale bude definován výraz pak nelze provést λy. y + 1. λx. x + y, λy. y + y, protože by se volná proměnná stala vázanou, a tím se změnil význam výrazu. Výrazy, které se liší pouze v α-konverzi, nazýváme α-ekvivalentní. [6] β-redukce je spojena s aplikací funkce. Konkrétně jde o substituci parametru volané funkce hodnotou. Použití je ukázáno na následujícím příkladu: (λx. x + 1) 9 -> (λx = 9. x + 1) -> 9 + 1 -> 10 Jakýkoliv lambda-výraz může být považován za hodnotu, a tedy aplikován na jiný lambda-výraz. Lze tedy napsat: [6] (λx. x + 1) (λy. y + 2) -> (λx = (λy. y + 2). x + 1) -> ((λy. y + 2) + 1) η-redukce se zabývá ekvivalencí funkcí. Říká, že dvě funkce jsou si rovny tehdy, a pouze tehdy, pokud vrací stejný výsledek pro všechny parametry. Následující příklad ukáže, jak tato konverze funguje. Je definován výraz λx. ((A) x),
4 Funkcionální programování 10 kde A označuje libovolný lambda-výraz. Celý výraz po aplikaci hodnoty lze zapsat jako což lze upravit na z čehož plyne, že (λx. ((A) x)) hodnota, (λx = hodnota. ((A) x))), neboli (A) hodnota, λx. ((A) x) = A. Výraz, který lze měnit s použitím některé z předchozích konverzí, se nazývá redex (reducible expression). Dle konverzí / redukcí se rozlišuje α, β nebo η-redex. [6] Zásadní myšlenkou lambda-kalkulu, který poté přebírají funkcionální programovací jazyky, je to, že pracuje s funkcí jako s hodnotou. Z toho mimo jiné vyplývá i to, že funkci lze předat jako parametr jiné funkci nebo vrátit funkci jako výsledek. 4.2 Charakteristické prvky funkcionálních programovacích jazyků Charakteristickým prvkům funkcionálních programovacích jazyků se ve své bakalářské práci [9] věnuje Igor Bobuský. Ve stručnosti je zde zopakováno, o jaké prvky se jedná. 4.2.1 Funkce jako entity první úrovně Jak bylo zmíněno na konci podkapitoly věnované lambda-kalkulu, funkcionální programovací jazyky přebírají z tohoto matematického systému koncept funkce jako hodnoty. Hodnotu lze uložit do proměnné, předávat jako parametr funkci, vracet jako výsledek funkce nebo uložit do datové struktury. Entity splňující předchozí kritéria se v anglickém jazyce nazývají first-class values nebo first-class citizens [3]. Do češtiny budu tyto výrazy překládat jako entity první úrovně a funkce první úrovně. Možnost pracovat s funkcí jako entitou první úrovně dodává psaní programů značnou flexibilitu. 4.2.2 Funkce vyššího řádu Jde o takové funkce, které jsou schopny provést alespoň jednu z následujících operací: [3] přijmout jednu nebo více funkcí jako parametr vrátit funkci jako výsledek
4 Funkcionální programování 11 V anglickém jazyce se tyto funkce nazývají higher-order functions [3]. V českém jazyce budu pro tyto funkce používat název funkce vyššího řádu. Funkce vyššího řádu umožňují zjednodušení kódu abstrahováním opakující se funkcionality. [3] 4.2.3 Odkazová transparentnost Tento princip souvisí s předchozím. V programování je odkazově transparentní takový výraz, který může být nahrazen svou hodnotou, aniž by to ovlivnilo chování programu [3]. Aplikováním tohoto přístupu na funkce lze volně zaměňovat volání funkce a vrácenou hodnotu. V anglickém jazyce je tento princip nazýván referential transparency [3]. V českém jazyce budu pro tento princip používat termín odkazová transparentnost. Tento princip je výhodný při optimalizaci provádění programu. 4.2.4 Neměnnost stavů Tento princip souvisí s předchozím. Čisté funkcionální programovací jazyky používají struktury, jejichž stav se nemění. Pokud by totiž byla umožněna změna struktur, závisel by výsledek funkce na čase, kdy by byla zavolána. Funkce by pak nemohla být odkazově transparentní. V anglickém jazce se používá výraz immutability. Do češtiny se překládá jako nemměnost [4]. Tento princip je výhodný při práci s vlákny. 4.2.5 Odložené vyhodnocení Jde o princip vyhodnocování výrazů až v okamžiku, kdy je jejich výsledek potřeba [3]. Neměnnost stavů dále umožňuje princip, který se v anglickém jazyce nazývá lazy-evaluation, který se do česekého jazyka překládá jako odložené vyhodnocení [4]. Výhodou je například možnost používání nekonečných datových struktur. 4.2.6 Absence vedlejších efektů Úlohou matematické funkce, dá-li se to takto vyjádřit, je přijmout jeden nebo více parametrů a vrátit výsledek, přičemž pro stejné parametry vrací vždy stejný výsledek. V programování se vedlejším efektem rozumí situace, kdy je provedena akce, která není zcela izolována v rámci dané funkce. Vedlejší efekty má funkce, která kromě vrácení výsledku přistupuje a mění globální proměnné, mění své vstupní parametry nebo má interakci s okolním prostředím může jít například o výpis na konzoli, čtení nebo zápis do souboru, vyhození výjimky nebo ukládání do databáze [30]. V angličtině se používá termín side-effect [3] který do češtiny překládáme jako vedlejší efekt [4]. Absence vedlejších efektů významným způsobem usnadňuje práci s vlákny.
4 Funkcionální programování 12 4.2.7 Deklarativní způsob programování Deklarativní způsob programování byl vysvětlen na začátku kapitoly. Jde o takový způsob programování, který se zaměřuje ne na způsob, ale na předmět řešení. Odpovídá na otázku, čeho má být dosaženo. Funkcionální programovací jazyky umožňují pracovat na vyšší úrovni abstrakce než imperativní a jsou více modulární. Opakující se funkcionalitu lze abstrahovat použitím funkcí vyššího řádu, čímž lze docílit kódu, který je srozumitelnější a více deklarativní. [3]
5 Funkcionální prvky v Javě 8 13 5 Funkcionální prvky v Javě 8 Funkcionální prvky v Javě 8 lze zjednodušeně rozdělit na dvě části. První se týká principu funkce jako hodnoty, druhý specifické datové struktury, která obsahuje prvky vytvořené pouze na požádání. 5.1 Funkce jako hodnota Tento prvek je novinkou Javy 8. V předchozích verzích Javy bylo třeba pro daný účel potřeba vytvořit třídu, jejíž instance funkci zabalila. Mohlo se jednat o instance klasických i anonymních tříd [4] anglicky anonymous class [1]. Funkci jako hodnotu můžeme nově předávat prostřednictvím lambda-výrazu nebo odkazu na metodu. Tyto konstrukce se v angličtině nazývají jako lambda-expressions a method references [1]. Jde o druh konstrukce, který se v anglickém jazyce nazývá syntactic sugar [3], v češtině se používá výraz syntaktický cukr. Překladač sice novou konstrukci převede na objekt, ale syntakticky se jeví jako funkce. Jedná se tedy pouze o zjednodušenou formu zápisu, ale tato zjednodušená forma velmi výrazně zpřehledňuje kód a usnadňuje jeho psaní. 5.1.1 Lambda-výrazy Lambda-výrazy jsou specifické konstrukce, které se použijí v situacích, kdy je třeba předat funkci jako hodnotu. Následující kód ukazuje stejnou funkcionalitu implementovanou za pomoci anonymní třídy a lambda-výrazu. * Anonymní třída implementuje funkční rozhraní IntFunction. Překrytá * metoda apply() přičte ke vstupnímu parametru hodnotu 1. public static IntFunction<Integer> fnc = new IntFunction<Integer>() { @Override public Integer apply(int x) { return x + 1; ; * Lambda-výraz implementuje funkční rozhraní IntFunction. Uložená funkce * přičte ke vstupnímu parametru hodnotu 1. public static IntFunction<Integer> fncadvanced = x -> x + 1;
5 Funkcionální prvky v Javě 8 14 Kód při použití lambda-výrazu je kratší a přehlednější. Odkazování na metody Může se stát, že požadovaná metoda je již k dispozici a je třeba ji pouze předat jako hodnotu. V těchto případech lze použít odkaz na metodu. Metoda přičítající hodnotu 1 ke vstupnímu parametru je například již deklarovaná např. ve třídě Vypocet. *************************************************************************** * Třída Vypocet obsahuje jedinou metodu prictijedna(). * Tato třída slouží k demonstraci odkazování na metodu. public class Vypocet { // Metoda přičítá ke vstupnímu parametru hodnotu 1. public static int prictijedna(int x) { return x + 1; Nyní následuje ukázka implementace pomocí anonymní třídy a odkazu na metodu. // Odkaz na metodu přičítající ke vstupnímu parametru hodnotu 1. public static IntFunction<Integer> fncreference = Vypocet::prictiJedna; Předchozí odkaz na metodu lze lambda-výrazem vyjádřit následujícím způsobem. // existující metodu lze vyjádřit lambda-výrazem public static IntFunction<Integer> fnc = x -> Vypocet.prictiJedna(x); Každý odkaz na metodu lze vyjádřit lambda-výrazem, ale ne každý lambda-výraz odkazem na metodu (metoda musí již existovat). 5.2 Seznam s odloženým vyhodnocením Seznam s odloženým vyhodnocením je charakteristickým prvkem funkcionálních programovacích jazyků. Jde o takovou datovou strukturu, která tvoří své prvky pouze na požádání. Důsledkem je, že může obsahovat nekonečnou sekvenci prvků. Anglický název pro tuto datovou strukturu je lazy-list [3]. Do češtiny budu tento termín překládat jako seznam s odloženým vyhodnocením.
5 Funkcionální prvky v Javě 8 15 5.2.1 Datovody Líné seznamy jsou v Javě 8 implementovány v podobě datovodů. Tento nový prvek ve spojení s výše popsanými lambda-výrazy a odkazy na metody umožňuje pracovat s daty výrazně snadnějším způsobem, než tomu bylo v předchozí verzi Javy. Na následujícím příkladu je ukázán kód zapsaný pomocí konstrukcí Javy 7 a 8. * Metoda přijímá seznam celých čísel a vypisuje čísla sudá. Metoda je * napsána bez pomoci datovodů. public static void printevennumbersold(list<integer> intlist) { for (int i : intlist) { if (i % 2 == 0) { System.out.println(i + " is even"); * Metoda přijímá seznam celých čísel a vypisuje čísla sudá. Metoda je * napsána s pomocí datovodů. public static void printevennumbersadvanced(list<integer> intlist) { intlist.stream().filter(i -> (i % 2 == 0)).forEach(i -> System.out.println(i + " is even")); V kódu Javy 8 není nutné se explicitně starat o procházení prvků a kód je více deklarativní.
6 Lambda-výrazy (a odkazy na metody) 16 6 Lambda-výrazy (a odkazy na metody) Je paradoxem, že se Java dočkala lambda-výrazů až s poslední verzí JDK 1.8, když některé populární programovací jazyky, a to nejen tradiční funkcionální jako Lisp nebo Scheme, je používaly již dávno předtím. Jedná se například o jazyky JavaScript, Python, Ruby, Groovy, Scala nebo C#. Tím spíše, že některé z nich běží na Java Virtual Machine. [10] O zařazení lambda-výrazů do Javy se začalo diskutovat již v roce 2006 po vydání JDK 1.5, ale nehledě na racionální argumenty k realizaci nedošlo. Myšlenka utonula v diskusích o budoucích směrech vývoje jazyka. V roce 2009 uvázla otázka na mrtvém bodě a zdálo se, že se Java lambda-výrazů nakonec nedočká. Naštěstí se v roce 2010 v Oracle rozhodli, že svůj jazyk nepohřbí, ale bude ho nadále podporovat jako populární široce používaný jazyk. [10] K dosažení tohoto cíle je třeba reagovat na tendence vývoje na poli informačních technologií, a to konkrétně multiprocesorového hardware. V dnešní době totiž ke zvyšování výkonu počítačů dochází spíše cestou kvantitativní než kvalitativní, tj. zvyšováním počtu procesorů [4]. Pro efektivní využití vícejádrové architektury je třeba operace zpracovávat paralelně. Aby mohl jazyk udržet krok s moderním trendem, bylo jasné, že knihovna kolekcí musí podstoupit výrazné změny. [10] Pro úspěšnou implementaci funkcí zpracovávající velké objemy dat paralelně je nezbytná jakási reorganizace odpovědností. Pokud by se měl programátor starat o to, jak má algoritmus fungovat, stál by před velmi nelehkým úkolem. Pro zjednodušení práce je třeba ustoupit od pohledu jak k pohledu co. Je tedy třeba provést posun od přístupu imperativního k přístupu deklarativnímu. Jedním z prvků, který tento přesun v nové verzi jazyka Java umožňuje, jsou právě lambda-výrazy. 6.1 Funkční interfejs Anglickým výrazem interface se v Javě rozumí specifická jazyková konstrukce podporující vytváření abstraktní vrstvy mezi konkrétními implementacemi a programy, které je používají. Do češtiny budu v souladu s [4] překládat tento termín jako interfejs. Functional interface [1] neboli funkční interfejs není novinkou Javy 8. S touto konstrukcí bylo možné se setkat již v předchozích verzích Javy. Jde například o interfejs Runnable, ActionListener, Comparator atd., tedy interfejs deklarující jedinou abstraktní metodu. S adopcí prvků funkcionálního programování se však začíná hovořit o funkčních interfejsech
6 Lambda-výrazy (a odkazy na metody) 17 a Java 8 pro ně zavádí anotaci @FunctionalInterface. Funkční interfejs je zajímavý, protože jej lze implementovat lambda-výrazem nebo odkazem na metodu. [1] Tvůrci jazyka se rozhodli nezavádět speciální datový typ jako v jiných jazycích (např. C#.NET). Místo toho se rozhodli, že se použije interfejs. V předchozích verzích Javy bylo třeba pro implementaci takového interfejsu použít pojmenovanou nebo anonymní třídu, která je ve srovnání s lambda-výrazem nebo odkazem na metodu mnohem upovídanější. Pro vytvoření instance funkčního interfejsu pomocí lambda-výrazu nebo odkazu na metodu, bude třeba znát jeho abstraktní metodu. Například funkční interfejs Function<T,R> deklaruje abstraktní metodu R apply(t t). Protože jde o funkční interfejs, lze jeho instanci vytvořit prostřednictvím lambda-výrazu nebo odkazu na metodu. Abstraktní metoda přijímá parametr typu T, a vrací výsledek typu R. Lze tedy napsat něco takového: [1] * Lambda-výraz implementuje funkční rozhraní Function. Definovaná funkce * umocní vstupní parametr typu Integer na druhou. Function<Integer, Integer> square = i -> i * i; * Lambda-výraz implementuje funkční rozhraní Function. Definovaná funkce * převede vstupní text typu String na text s velkými písmeny. Function<String, String> uppercase = s -> s.touppercase(); 6.2 Syntaxe lambda-výrazů Lambda-výrazy se v Javě zapisují s pomocí šipky ->. Vlevo od šipky se nachází parametry, vpravo tělo funkce. Zde jsou některé příklady zápisu lambda-výrazů [1]. V případě že metoda nemá parametry, zapisují se kulaté prázdné závorky. // lambda-výraz bez parametrů s návratovou hodnotou void () -> System.out.println("Hello, world."); Pokud má metoda parametr, jehož typ je schopen překladač odvodit z kontextu, není nutné typ explicitně uvádět. Pokud je parametr pouze jeden, lze vynechat kulaté závorky. // lambda výraz s jedním parametrem s návratovou hodnotou void e -> System.out.println(e); Metoda může mít pochopitelně více parametrů. V některých případech není překladač schopen z kontextu zjistit typ parametru, a tak je nutní jej explicitně uvést. V případě, že je v těle potřeba uvést více příkazů, použije se zápis v bloku se složenými závorkami.
6 Lambda-výrazy (a odkazy na metody) 18 // lambda-výraz se dvěma parametry s tělem zapsaným v bloku (String first, String second) -> { if (first.length() < second.length()) return -1; else if (first.length() > second.length()) return 1; else return 0; 6.3 Odkazování na metody Lambda-výrazy pomáhají vytvářet anonymní metody. Někdy ale pouze volají nějakou již existující metodu. V těchto případech lze použít tzv. odkaz na metodu. [1] Odkaz na metodu zapisujeme za použití operátoru :: následujícím způsobem: // statická metoda nebo metoda instance SomeClass::someMethod // metoda instance someinstance::somemethod // konstruktor třídy someclass SomeClass::new V prvním případě se jedná o metodu somemethod() definovanou ve třídě SomeClass. Může jít o statickou i instanční metodu. V případě instanční metody je třeba v prvním parametru uvést instanci dosazovanou za this. Ve druhém výrazu je odkaz na metodu somemethod() instance someinstance a v poslední ukázce na konstruktor třídy SomeClass [4]. Pokud jsou metody přetížené, překladač si vybere správnou verzi. V následujícím příkladu se filtrují všechny osoby s vysokoškolským vzděláním. * Metoda testovací třídy PrikladyTest vytváří testovací seznam * uživatelů. public List<Person> createlistofpeople() { Person p1 = new Person("Jan", "Novák", 35, "Praha", PRIMARY_SCHOOL); Person p2 = new Person("Jiří", "Novotný", 29, "Brno", HIGH_SCHOOL); Person p3 = new Person("Martin", "Kovář", 25, "Olomouc", UNIVERSITY); Person p4 = new Person("Petr", "Kováč", 31, "Plzeň", UNIVERSITY); List<Person> people = new ArrayList(); people.add(p1); people.add(p2); people.add(p3);
6 Lambda-výrazy (a odkazy na metody) 19 people.add(p4); return people; Zde je metoda pro filtrování. * Metoda přijímá seznam instancí typu Person - osoby a instanci typu * IPersonPredicate a vrací seznam instancí třídy Person - osoby splňující * daný predikát. public static List<Person> filterpeople(list<person> list, IPersonPredicate p) { List<Person> result = new ArrayList<>(); for (Person person : list) { if (p.test(person)) { result.add(person); return result; Metoda se testuje následovně. @Test public void testfilterpeople() { List<Person> people = createlistofpeople(); List<Person> result = Priklady.filterPeople(people, p -> p.iseducated()); asserttrue(result.size() == 2); Lambda-výraz použitý v testu ale nedělá nic jiného, než že definuje příkaz, který metodu iseducated() zavolá, až někdo ve volané metodě předávaný lambda-výraz aktivuje. Lze jej tedy přepsat s pomocí odkazu na metodu takto: ArrayList<Person> educatedpeople = filterpeople(list, Person::isEducated); V některých případech může být zápis při použití odkazu na metodu přehlednější.
6 Lambda-výrazy (a odkazy na metody) 20 6.4 Od parametrizace hodnot k lambdám Tato podkapitola bude věnována praktické ukázce užitečnosti konstrukcí umožňujících předávat funkci jako hodnotu. Je předloženo zadání na následující implementaci. Personální oddělení společnosti žádá o dodávku systému, který by kromě jiného poskytoval řešení filtrování osob podle zadaných kritérií, přičemž osoby musí být možné filtrovat i podle více kritérií zároveň, tj. například podle vzdělání, věku atd. První pokus Na první pohled se jedná o triviální záležitost. Lze začít například s implementací, která umožní filtrovat osoby podle vzdělání. Kód by mohl vypadat následovně: * Metoda přijímá seznam instancí typu Person - osoby a vrací seznam * instancí typu Person - osoby splňující pevně nastavenou podmínku * vzdělání. public static List<Person> educatedpeople(list<person> list) { ArrayList<Person> result = new ArrayList<>(); for (Person person : list) { if (person.geteducation().equals(university)) { result.add(person); return result; Po pozornějším prohlédnutí je ale vidět, že řešení je poměrně nešikovné. Metoda má pouze jeden parametr a v těle metody je pevně nastavena konkrétní hodnota vzdělání. Pro nalezení zaměstnance s jiným vzděláním by bylo třeba mít podobnou metodu pro každou hodnotu vzdělání. Jeden ze základních principů programování říká, že bezdůvodné opakování kódu je chybou [4]. Pro návrh dobrého kódu je třeba se řídit principem DRY (Don t Repeat Yourself). Je tedy nutné najít lepší řešení. Druhý pokus Metodu lze vylepšit tak, že hodnota nebude zadávána pevně do těla metody, ale bude předávána jako parametr. * Metoda přijímá seznam instancí typu Person - osob a hodnotu výčtového * typu Education - vzdělání a vrací seznam instancí typu Person - osoby
6 Lambda-výrazy (a odkazy na metody) 21 ****************************************************************************** * Funkční rozhraní IPersonPredicate se využívá pro parametrizaci chování. * * @author Maxim Rytych * splňující danou podmínku vzdělání. public static ArrayList<Person> personeducation(arraylist<person> list, Education education) { ArrayList<Person> result = new ArrayList<>(); for (Person person : list) { if (person.geteducation().equals(education)) { result.add(person); return result; Kód je nyní generičtější. Druhý parametr umožňuje zadávat požadované vzdělání. Zanikl tedy problém, kvůli kterému bylo nutné mít pro každou úroveň vzdělání samostatnou metodu. Nyní lze přejít k implementaci metody, která bude filtrovat osoby podle dalšího kritéria například věku. * Metoda přijímá seznam instancí typu Person - osob a hodnotu typu * int - věk a vrací seznam typu Person - osoby splňující danou podmínku * věku. public static List<Person> personage(list<person> list, int age) { ArrayList<Person> result = new ArrayList<>(); for (Person person : list) { if (person.getage() < age) { result.add(person); return result; Do metody pro filtrování podle věku byl stejně jako v předchozí metodě přidán parametr, který umožňuje zadat požadovaný věk. Zbytečnému opakování kódu se tedy lze vyhnout vhodnou parametrizací. Třetí pokus Zvyšování počtu parametrů a/nebo metod ale také není zcela optimálním řešením. Je třeba provést vhodnou abstrakci, která umožní pohlédnout na řešený problém z nového úhlu. Novým řešením může být zavedení interfejsu.
6 Lambda-výrazy (a odkazy na metody) 22 * @version 1.00.0000 2015-12-06 @FunctionalInterface public interface IPersonPredicate { boolean test(person person); Interfejs obsahuje jednu abstraktní metodu s návratovou hodnotou typu boolean a jedním parametrem typu Person. Protože má pouze jednu abstraktní metodu, jde o funkční interfejs, který lze označit anotací @FunctionalInterface. Interfejs implementují následující třídy. **************************************************************************** * Třída EducatedPeople implementuje funkční interfejs IPersonPredicate. * Třída se využívá pro parametrizaci chování. Obsahuje v sobě "chování" v * podobě metody test(). * * @author Maxim Rytych * @version 1.00.0000 2015-12-06 public class EducatedPeople implements IPersonPredicate { * Metoda test() přijímá instanci typu Person - osobu a vrací hodnotu * typu boolean na základě vyhodnocení podmínky - zda má osoba * vysokoškolské vzdělání. @Override public boolean test(person person) { return person.geteducation().equals(university); ****************************************************************************** * Třída EducatedPeople implementuje funkční interfejs IPersonPredicate. * Třída se využívá pro parametrizaci chování. Obsahuje v sobě "chování" v * podobě metody test(). * * @author Maxim Rytych * @version 1.00.0000 2015-12-06 public class YoungPeople implements IPersonPredicate {
6 Lambda-výrazy (a odkazy na metody) 23 Metoda test() přijímá instanci typu Person - osobu a vrací hodnotu * typu boolean na základě vyhodnocení podmínky - zda je osoba mladší * třiceti let. @Override public boolean test(person person) { return person.getage() < 30; ****************************************************************************** * Třída EducatedPeople implementuje funkční interfejs IPersonPredicate. * Třída se využívá pro parametrizaci chování. Obsahuje v sobě "chování" v * podobě metody test(). * * @author Maxim Rytych * @version 1.00.0000 2015-12-06 public class YoungAndEducatedPeople implements IPersonPredicate { * Metoda test() přijímá instanci typu Person - osobu a vrací hodnotu * typu boolean na základě vyhodnocení podmínky - zda je osoba mladší * třiceti let a má vysokoškolské vzdělání. @Override public boolean test(person person) { return ((person.getage() < 30) && (person.geteducation().equals(university))); První třída slouží pro vyhledávání osob s vysokoškolským vzděláním, druhá pro vyhledávání osob mladší třiceti let. Třetí implementace kombinuje oba případy. Samotnou metodu pro filtrování osob lze upravit následujícím způsobem: * Metoda přijímá seznam instancí typu Person - osoby a instanci typu * IPersonPredicate a vrací seznam instancí třídy Person - osoby splňující * daný predikát. public static List<Person> filterpeople(list<person> list, IPersonPredicate p) { List<Person> result = new ArrayList<>();
6 Lambda-výrazy (a odkazy na metody) 24 for (Person person : list) { if (p.test(person)) { result.add(person); return result; Druhý parametr přijme instanci typu IPersonPredicate. V podmínce v těle cyklu for se zavolá metoda test() konkrétní implementace s parametrem typu Person. Na základě vyhodnocení podmínky se přidávají osoby do výsledného seznamu. Metodu lze otestovat následujícím způsobem. @Test public void testyoungandeducatedpeople() { List<Person> people = createlistofpeople(); List<Person> result = Priklady.filterPeople(people, new YoungAndEducatedPeople()); asserttrue(result.size() == 1); Toto volání do seznamu přidá pouze osoby mladší třiceti let s vysokoškolským vzděláním. Protože je v seznamu pouze jedna taková osoba, test proběhne úspěšně. Obdobně lze volat metodu s dalšími případnými implementacemi interfejsu IPersonPredicate. Oproti předchozím řešením se podařilo dosáhnout významného pokroku. Podařilo se dosáhnout něčeho, co se v angličtině nazývá behavior parameterization [2]. Česky lze tento termín přeložit jako parametrizace chování. Znamená to, že byla oddělena logika samotné metody pro filtrování a chování, které má být metodou realizováno. Nyní lze prostřednictvím tříd implementujících interfejs IPersonPredicate předávat požadované chování, aniž by bylo nutné metodu přepisovat. Nyní je dokonce možné filtrovat i podle více kritérií najednou. Čtvrtý pokus Pokud by se jednalo o chování, které by se mělo používat na více místech, bylo by řešení téměř optimální. Vylepšit by se dalo pouze použitím odkazu na metodu. Pokud by se ale jednalo o ad-hoc použití, bylo by možné návrh optimalizovat použitím anonymní třídy. Anonymní třídy zná jistě přinejmenším každý vývojář, který se někdy zabýval návrhem grafického uživatelského rozhraní. Používají se například pro nastavení chování komponenty např. stisku tlačítka. Jedná se o konstrukce, které umožňují ve stejný
6 Lambda-výrazy (a odkazy na metody) 25 moment deklarovat objekt a vytvořit jeho instanci. Podobné konstrukce jsou vhodné, když je třeba implementovat nějakou ad-hoc funkcionalitu. Test lze za pomoci anonymní třídy napsat následujícím způsobem: @Test public void testyoungandeducatedpeopleviaanonymousclass() { List<Person> people = createlistofpeople(); List<Person> result = Priklady.filterPeople(people, new IPersonPredicate() { ); @Override public boolean test(person person) { return ((person.getage() < 30) && (person.geteducation().equals(university))); asserttrue(result.size() == 1); Jak je vidět, metodě byla jako druhý parametr předána instance anonymní třídy. Nyní tedy není třeba vytvářet třídu samostatně, ale lze ji vytvořit přímo na místě použití. Pokud by šlo o kód, který by byl použit častěji, lze použít klasickou třídu, jak bylo ukázáno v příkladu nazvaném třetí pokus. Řešení není ale ještě ani v tuto chvíli zcela optimální. Hlavními výhradami bude jistě to, že kód je stále poměrně rozsáhlý. Nevytváří se sice nová třídu samostatně, ale její tělo je stále používáno. Další výhradou je nepřehlednost anonymních tříd. Kód by měl být ze své podstaty čitelný na první pohled, což se o poslední ukázce spíše říci nedá. Existuje ještě jiné řešení, které by kód více zestručnilo a zpřehlednilo? Odpověď je ano. Lze toho dosáhnout použitím nových prvků jazyka Java 8 lambda-výrazů nebo odkazů na metody. Pátý pokus Testovací metodu lze upravit s pomocí lambda-výrazu následovně. @Test public void testyoungandeducatedpeoplevialambda() { List<Person> people = createlistofpeople(); List<Person> result = Priklady.filterPeople(people, (Person person) -> ((person.getage() < 30) && (person.geteducation().equals(university)))); asserttrue(result.size() == 1);
6 Lambda-výrazy (a odkazy na metody) 26 Aby bylo možné použít odkaz na metodu, je nutné nejprve odpovídající metodu vytvořit. Metoda přijme instanci typu Person - osobu a vrátí hodnotu typu * boolean v závislosti na vyhodnocení podmínky - zda je osoba mladší * třiceti let a má vysokoškolské vzdělání. public static boolean test(person person) { return (person.getage() < 30 && person.geteducation().equals(university)); Testová metoda s odkazem na metodu bude téměř stejná jako předchozí, kde byl použit lambda-výraz. @Test public void testyoungandeducatedpeopleviamethodreference() { List<Person> people = createlistofpeople(); List<Person> result = Priklady.filterPeople(people, Priklady::test); asserttrue(result.size() == 1); Zápis prostřednictvím odkazu na metodu je sice nejkratší a nejpřehlednější, ale má tu nevýhodu, že metoda musí být již někde implementována. Lambda-výraz může být méně přehledný, ale lze jej implementovat v místě použití. Parametrizace hodnot ukázala, že je schopna jistým způsobem redukovat napsaný kód. Příkladem byla situace, kdy přidáním dalšího parametru do metody pro filtrování osob bylo možné filtrovat všechny možnosti v konkrétní oblasti, tj. bylo například možné vyhledávat osoby podle vzdělání v jedné metodě. Vzápětí se však ukázalo, že toto řešení nestačí. Nebylo totiž jeho použitím možné vyřešit problém se zadáváním více kritérií. Parametrizace hodnot byla nahrazena parametrizací chování, které předchozí problém dovolilo řešit. Od parametrizace chování se vlastně řešilo předávání metod jako parametru, ačkoli to nebylo takto explicitně vyjádřeno. Protože nešlo vytvořit instanci přímo, bylo nutné ji zabalit do nějakého objektu. Jako první řešení byl použit interfejs a metody byly předávány prostřednictvím tříd implementujících tento interfejs. Toto řešení bylo poté nahrazeno anonymními třídami, které umožnily zbavit se samostatných implementací, takže šlo třídu s metodou deklarovat přímo v místě použití.