Funkcionální syntaxe v objektové Javě O co jde? • Java byla navržena jako čistě objektový jazyk. • Lambda výrazy do tohoto jazyka přináší prvky funkcionálního programování. • Jak mohou tyto dva různé světy koexistovat? • Proč a jak je funkcionální syntaxe integrována do objektového API Javy? Motivační příklad  Založeno na Java Tutorial • Předpokládejme, že chceme vytvořit sociální síť. Administrátorům chceme umožnit provádění různých akcí jako například zasílání zpráv těm uživatelům sociální sítě, kteří splňují nějaká kritéria. • Předpokládejme, že uživatelé jsou reprezentování následující třídou: public class Person {   public enum Sex {   MALE, FEMALE   }   private String name;   private LocalDate birthday;   private Sex gender;   private String emailAddress;   public int getAge() {   // ...   }   public void printPerson() {   // ...   } } • Dále předpokládejme, že jsou uživatelé sociální sítě uložení v seznamu List. 1 Krok 1: Primitivní řešení • Základní implementace vyhledávání lidí podle kritéria může vypadat následovně: public static void printPersonsOlderThan(List roster, int age) {   for (Person p : roster) {   if (p.getAge() >= age) {   p.printPerson();   }   } } • Q: Co když chceme přidat další operaci, např. výpis lidí mladších než nějaký věk? • A: Musí se přidat nová metoda, nebo se existující metoda musí pojmout víc obecněji. Krok 2: Zobecněné vyhledávání public static void printPersonsWithinAgeRange(List roster, int low, int high) {   for (Person p : roster) {   if (low <= p.getAge() && p.getAge() < high) {   p.printPerson();   }   } } • Q: Co když chceme vypisovat uživatele specifického pohlaví, nebo dokonce kombinace věk a pohlaví? • Q: Co když se rozhodneme změnit třídu Person a přidat do ní atributy, např. vzájemné vztahy nebo geografická lokace? • A: Přestože je tato metoda obecnější než předchozí printPersonsOlderThan, snaha vytvořit specifickou metodu pro každý možný vyhledávací dotaz je neudržitelná. • Řešení: Oddělit kód, který specifikuje vyhledávací kritéria, od samotného vyhledávání.  Jména metod musí být výstižná a popisná. Proto byla metoda přejmenována. Krok 3: Použití vlastního rozhraní [1/2] • Definujeme rozhraní pro vyhledávací a vytvoříme implementaci: 2 interface CheckPerson {   boolean test(Person p); } class CheckPersonEligibleForSelectiveService implements CheckPerson {   public boolean test(Person p) {   return p.gender == Person.Sex.MALE &&   p.getAge() >= 18 &&   p.getAge() <= 25;   } }  Ve stejnou chvíli můžeme mít definováno několik vyhledávacích kritérií (tříd implementujících rozhraní). Krok 3: Použití vlastního rozhraní [2/2] • Vyhledávací metoda se změní následovně: public static void printPersons(List roster, CheckPerson tester) {   for (Person p : roster) {   if (tester.test(p)) {   p.printPerson();   }   } } • A volá se takto: List roster = ... printPersons(roster, new CheckPersonEligibleForSelectiveService()); • Q: Je nutné definovat CheckPersonEligibleForSelectiveService ve speciální třídě? • A: Není. Můžeme použít anonymní třídy a redukovat tak kód. Krok 4: Použití anonymní třídy 3 interface CheckPerson {   boolean test(Person p); } public static void printPersons(List roster, CheckPerson tester) {   for (Person p : roster) {   if (tester.test(p)) {   p.printPerson();   }   } } List roster = ... printPersons(   roster,   new CheckPerson() {   public boolean test(Person p) {   return p.getGender() == Person.Sex.MALE   && p.getAge() >= 18   && p.getAge() <= 25;   }   } ); • Všimněte si, že CheckPerson je funkcionální - obsahuje jedinou metodu. Proto je ale název metody nepodstatný. • Q: Mohli bychom název metody nějak vynechat? • A: Ano, pokud použijeme lambda výraz, který se dá chápat jako definice anonymní metody implementující nějaké funkcionální rozhraní. Krok 5: Použití lambda výrazu 4 interface CheckPerson {   boolean test(Person p); } public static void printPersons(List roster, CheckPerson tester) {   for (Person p : roster) {   if (tester.test(p)) {   p.printPerson();   }   } } List roster = ... printPersons(   roster,   (Person p) -> p.getGender() == Person.Sex.MALE   && p.getAge() >= 18   && p.getAge() <= 25 ); • Levá část (před šipkou) obsahuje vstupní parametry, pravá strana pak kód případně výstupní hodnotu. • Všimněte si, že rozhraní CheckPerson se teď v kódu objevuje pouze typ argumentu v metodě printPerson(). Ale název typu (ani metody, jak už víme) není důležitý. • Q: Mohli bychom vynechat z kódu i typ CheckPerson? • A: Ano. Java nabízí pro takové případy vlastní generická předdefinovaná rozhraní. Krok 6: Použití existujících funkcionálních rozhraní [1/3] • Zamysleme se nad původní definicí našeho rozhraní: interface CheckPerson {   boolean test(Person p); } • Jeho význam můžeme zobecnit pomocí generických typů podobně, jako je tomu u rozhraní java.util.functions.Predicate: interface Predicate {   boolean test(T t); } 5 Krok 6: Použití existujících funkcionálních rozhraní [2/3] • CheckPerson tedy již nadále nepotřebujeme. Můžeme místo něj použít Predicate: public static void printPersons(List roster, Predicate tester) {   for (Person p : roster) {   if (tester.test(p)) {   p.printPerson();   }   } } List roster = ... printPersons(   roster,   (Person p) -> p.getGender() == Person.Sex.MALE   && p.getAge() >= 18   && p.getAge() <= 25 ); Krok 6: Použití existujících funkcionálních rozhraní [3/3] • Zamysleme se nad rozhraním Predicate a jeho použitím ještě jednou: interface Predicate {   boolean test(T t); } ... (Person p) -> p.getGender() == Person.Sex.MALE   && p.getAge() >= 18   && p.getAge() <= 25 ... • Všimněte si, že rozhraní Predicate používá typ T pouze na zjištění typu vstupního argumentu metody. Jenže typ objektu, který metodě posíláme, může být (a je) rozpoznán za běhu. Takže tato informace je v podstatě nedůležitá. • Q: Mohli bychom deklaraci typu při volání vynechat? • A: Ano. Následující fragment kódu je rovněž správně. Že T odpovídá Person zjistí Java interpret 6 za běhu. p -> p.getGender() == Person.Sex.MALE   && p.getAge() >= 18   && p.getAge() <= 25 Jaká máme funkcionální rozhraní v Javě? • Rozhraní v java.util.function package, například Predicate s jednou metodou boolean test(T t), Supplier s jednou metodou void get(T t), Consumer s jednou metodou void accept(T t). • Další rozhraní, která sice mohou definovat více metod, ale jen jedna zni je nestatická, například ◦ Comparator z java.util, ◦ Iterable z java.lang. Příklad použití dalších funkcionálních rozhraní • Naše současná implementace vyhledávací metody vypisuje informace o osobách splňujících predikát: public static void printPersons(List roster, Predicate tester) {   for (Person p : roster) {   if (tester.test(p)) {   p.printPerson();   }   } } • Co když ale s osobami, které splňují predikát daný parametrem tester, chceme dělat něco jiného, než jen vypisovat informace o nich? Funkcionální rozhraní Consumer • Funkcionální rozhraní Consumer s metodou void accept(T t) nabízí obecnou operaci na 7 zpracování objektu. • Jako implementaci rozhraní Consumer lze použít jakýkoliv kód, který na daném objektu něco vykoná, ale nic nevrací. • Velmi často se prostě zavolá bezparametrická metoda definovaná na daném objektu, v našem případě p.printPerson(). public static void processPersons(   List roster,   Predicate tester,   Consumer block) {   for (Person p : roster) {   if (tester.test(p)) {   block.accept(p);   }   } } processPersons(   roster,   p -> p.getGender() == Person.Sex.MALE   && p.getAge() >= 18   && p.getAge() <= 25,   p -> p.printPerson() ); Funkcionální rozhraní Function • Co když nám nestačí zpracovat původní objekty Person, ale rádi bychom z nich vytáhli nějaké informace, například e-mail, a teprve tyto informace zpracovali? • K tomu potřebuje rozhraní, které by vracelo nějakou hodnotu. • Funkcionální rozhraní Function nabízí metodu R apply(T t), která slouží k "transformaci" dat typu T na data typu R. 8 public static void processPersons(   List roster,   Predicate tester,   Function mapper,   Consumer block) {   for (Person p : roster) {   if (tester.test(p)) {   String data = mapper.apply(p);   block.accept(data);   }   } } processPersons(   roster,   p -> p.getGender() == Person.Sex.MALE   && p.getAge() >= 18   && p.getAge() <= 25,   p -> p.getEmailAddress(),   email -> System.out.println(email) ); Extenzivní využití generických typů [1/2] • Metoda processPersons sice pracuje pouze se dvěma typy (Person and String), ale její smysl se dá zobecnit takto: procházej objekty, vyber, které splňují predikát, vezmi z nich nějaká data, a tato data zpracuj. • Tohoto zobecnění lze dosáhnout zobecněním typů za použití generik: public static void processElements(   Iterable source,   Predicate tester,   Function mapper,   Consumer block) {   for (X p : source) {   if (tester.test(p)) {   Y data = mapper.apply(p);   block.accept(data);   }   } } 9 Extenzivní využití generických typů [2/2] • Vypsání e-mailových adres lidí pak lze vypsat stejně jako předtím: processElements(roster,   p -> p.getGender() == Person.Sex.MALE   && p.getAge() >= 18   && p.getAge() <= 25,   p -> p.getEmailAddress(),   email -> System.out.println(email) ); • Co se ale změnilo je to, že lze takto zpracovat i jiné třídy/objekty, než jen Person! Datové proudy (streams) • Rozhraní java.util.stream.Stream používá nastíněné principy a nabízí jednoduché metody, které mohou být použité pro proudové zpracování dat. Náš kód lze přepsat takto: roster.stream()   .filter(   p -> p.getGender() == Person.Sex.MALE   && p.getAge() >= 18   && p.getAge() <= 25)   .map(p -> p.getEmailAddress())   .forEach(email -> System.out.println(email)); Principy proudového zpracování dat • Java Core API nabízí jednoduchá funkcionální rozhraní vhodná pro proudové zpracování dat. • Díky generickým typům jsou tato rozhraní jsou nezávislá na tom, jaká data jsou v proudu uložena. • Rozhraní Stream poskytuje metody, které využívají funkcionální rozhraní pro definici jednoduchých operací nad proudovými daty. Operace je aplikována na všechny objekty proudu. • Většina těchto metod vrací "výsledný" proud jako výstupní hodnotu, takže lze operace snadno řetězit. • Vývojáři mohou využít anonymní třídy pro snadnou definici proudových operací (implementaci požadovaných rozhraní). Protože se ale jedná o funkcionální rozhraní, lze navíc použít kompaktní zápis pomocí lambda výrazů. • Resumé: Vývojáři mohou implementovat proudově-orientované zpracování dat s použitím přístupu podobného funkcionálním jazykům. 10