Toto je druhý zo série článkov o tom, čo je nové v Jave 8. V tom prvom sme si rozobrali lambda metódy. Teda možnosť, ako zadefinovať anonymnú metódu na jedno použitie. Nové spôsoby metód pri tomto ale nekončia. Dnes si ukážeme, že v novej Jave sú metódy naozaj first-class citizen, a tiež to, že rozhrania už nie sú to, čo bývali.
V prvom dieli sme si ukázali, že Lambda výraz je vlastne anonymná metóda, ktorá sa dá poslať ako parameter do inej metódy. Napríklad:
doSomething(e -> System.out.println(x));
Otázka, ktorá vás ale môže trápiť je, ako vlastne vyzerá definícia metódy doSomething a hlavne jej parametra? Existuje nejaký nový typ, ktorý reprezentuje takúto lambda výraz metódu? Odpoveď je nie. Ale metódu po novom zastupuje špeciálny typ rozhrania – funkcionálne rozhranie.
Funkcionálne rozhranie je rozhranie, ktoré má len jednu abstraktnú metódu – teda metódu bez definovaného obsahu. Napríklad nové rozhranie Consumer, ktoré vieme nájsť v Jave v balíku java.util.function:
@FunctionalInterface public interface Consumer<T extends Object> { public void accept(T t); }
Toto rozhranie má jednu metódu, ktorá ako parameter berie práve jeden objekt a nič nevracia. Ak si všimnete lambda výraz vyššie (System.out.println(x)), tak to presne spĺňa to, čo metóda accept sľubuje. Ak to všetko dáme dokopy, metóda doSomething by mohla vyzerať takto:
public void doSomething(final Consumer<String> consumer) { String text = „hello“; consumer.accept(hello); }
Čo sa to stalo s Lambdou v metóde doSomething? Premenila sa na objekt, ktorý implementuje rozhranie! Áno, takto nejako to funguje. Na jednej strane popis funkcie. Na tej druhej, objekt definovaný cez rozhranie. Zjednodušene (a obrazne) by sa dalo povedať, že Java vezme lambda výraz a rozhranie, ktoré má reprezentovať. Na pozadí zadefinuje triedu, ktorá implementuje dané rozhranie a na implementáciu jeho jedinej metódy použije Lambda výraz. Následne vyrobí inštanciu tejto triedy a pošle ju ako parameter do metódy doSomething.
Čo to všetko znamená? Napríklad aj to, že ako parameter metódy musí byť použité rozhranie s jednou abstraktnou metódou. Ak by rozhranie malo metód viac, hrozí, že pri vyrábaní implementácie rozhrania Java nebude vedieť, ktorú metódu v rozhraní má Lambda výraz zastupovať. A tiež to znamená, že z odkazov na metódy v prvom bode pri ich preposlaní sa stávajú vlastne objekty.
Poďme ale späť k tomu rozhraniu s jednou metódou. Ako som tu už skôr spomínal, ide o takzvané funkcionálne rozhranie. Aby to bolo jasné, aj v Jave je na to nová anotácia @FunctionalInterface. Tá nie je povinná a slúži hlavne, abyaj kompilátor vedel, čo s rozhraním zamýšľate, a ak tam bude viac abstraktných metód, tak hneď zahlásiť chybu.
Funkcionálne rozhranie teda nemusí byť špeciálne ničím iným ako len tým, že má práve jednu abstraktnú metódu. Ja viem, dookola tu píšem o abstraktnej metóde, ako keby Java rozhrania mohli mať aj iné ako abstraktné metódy. Tak to bývalo vo všetkých predchádzajúcich verziách. Java 8 má aj v tomto smere novinku.
V tejto verzii je totiž možné zadefinovať niečo, čo sa nazýva defaultná metóda. To je metóda, ktorá má aj svoje telo. A teda nie je abstraktná. Vlastne to vyzerá ako klasická metóda niekde v triede, akurát, že je v rozhraní. Pred chvíľou som uviedol rozhranie Consumer. Po pravde som ho trochu osekal. Reálne to rozhranie obsahuje metódy dve:
@FunctionalInterface public interface Consumer<T extends Object> { public void accept(T t); public default Consumer<T> andThen(Consumer<? super T> cnsmr) { ... } }
Druhá metóda je ale defaultná (všimnite si kľúčové slovo defaultv jej definícii). To v praxi znamená, že ak budete implementovať toto rozhranie, nemusíte implementovať danú metódu. Aj preto sa potom toto rozhranie dá použiť s Lambda výrazmi. V skutočnosti, aj keď má metód viac, podstatné je, že má len jednu abstraktnú.
S defaultnými metódami si Java otvorila dvere (a možno pandorinu skrinku) k niečomu, čo nazýva viacnásobné dedenie implementácie. Výhody a úskalia tohto princípu by stačili na vlastný článok a tak to tu pre tento prípad vynechám. Čo vás ale napadne, môže byť otázka, na čo je dobré? Prečo by som chcel definovať rozhranie, ktoré už má metódy. Odpoveď je spätná kompatibilita.
Java 8 so sebou prináša naozaj pomerne veľa veľkých zmien. A niektoré sú tak veľké, že je potrebné upraviť aj základné kamene, na ktorých Java stojí. To sú napríklad rozhrania ako List, Map a pod. Kvôli novém Collection API (o ktorom si povieme niekedy nabudúce) bolo potrebné do týchto rozhraní pridať nové metódy. Problém je, že je to zásah do rozhrania, ktorý spôsobí problém všetkým implementáciám tam vonku. A za tú dobu ich je pre tieto rozhrania naozaj dosť. Takže sa Oracle rozhodol, že nebude hladkať tigra proti srsti a pridal do týchto rozhraní defaultné metódy, ktoré nevyžadujú žiaden nový development.
Mohlo by sa zdať, že tých zmien ohľadom metód už bolo aj dosť, ale ešte stále to nie je všetko. Ukázali sme si, ako sa definujú anonymné metódy a ako sa preposielajú. Preposielať sa ale nemusia len anonymné metódy. A práve teraz na scénu vstupujú referencie na metódy.
Pre názornú ukážku si zadefinujme triedu Car:
public class Car { public static Car create(final Supplier<Car> supplier) { … } public static void collide(final Car car) { … } public void follow(final Car another) { … } public void repair() { … } }
Ukážkovú triedu by sme mali. Poďme si teraz ukázať, ako sa vieme odkazovať na jej metódy. Ako prvé je to odkaz na klasickú inštančnú metódu:
cars.forEach(Car::repair);
Dve dvojboky za sebou sú zase syntaktickou novinkou v Jave. Na inštančnú metódu sa vieme odkázať aj cez inštanciu:
final Car police = new Car(); cars.forEach(police::follow);
Všimnite si, že metóda follow podľa definície potrebuje jeden parameter typu triedy Car. Ten ale v tomto kóde neposielam. Pošle sa tam automaticky. Teda ak volám inštančnú metódu nad inštanciou, tak tá metóda ju dostane ako parameter.
Takto vyzerá volanie statickej metódy:
cars.forEach(Car::collide);
A na záver si pozrieme odkaz na konštruktor:
final Car car = Car.create(Car::new);
Konštruktor je teda tiež metóda, na ktorú sa dá odkazovať. Namiesto názvu metódy sa použije kľúčové slovo new.
Aký je vlastne rozdiel medzi Lambda výrazmi a referenciami na metódu? Veľký nie. Vlastne len to, že Lambda výraz nemá meno, a tak sa na nich nedá odkazovať mimo toho, kde sú použité. Referencie na metódy meno majú, a teda sú viackrát použiteľné (a vďaka tomu aj lepšie identifikovateľné pri čítaní kódu). Inak sa s nimi pracuje rovnako. Odporúčaný postup je taký, že ak potrebujete metódu použiť na jednom mieste, použite Lambdu. Ak na viacerých, zadefinujete ju niekde a použijete referencie na ňu.
Ako je vidno, Java 8 neberie metódy na ľahkú váhu. Prináša široký arzenál možností, ktorý určite zakýva doterajšími postupmi vývoja (myslím si, že už aj základná sada návrhových vzorov po tomto dostane svoj update). Na všetky tieto novinky sa treba pozerať ako na nový nástroj, ktorý stojí za to poznať a aj použiť. Obzvlášť zaujímavé je to v spojení s novým Collection API, o ktorom si povieme nabudúce.