Funkčné rozhrania v prostredí Java 8

1. Úvod

Tento článok je sprievodcom rôznymi funkčnými rozhraniami v prostredí Java 8, ich všeobecnými prípadmi použitia a použitím v štandardnej knižnici JDK.

2. Lambdas v prostredí Java 8

Java 8 priniesla nové silné syntaktické vylepšenie v podobe výrazov lambda. Lambda je anonymná funkcia, ktorú je možné spracovať ako občana prvotriedneho jazyka, napríklad odovzdať alebo vrátiť z metódy.

Pred jazykom Java 8 by ste zvyčajne vytvorili triedu pre všetky prípady, keď je potrebné zapuzdriť jednu funkcionalitu. To znamenalo veľa zbytočného štandardného kódu na definovanie niečoho, čo slúžilo ako primitívne znázornenie funkcie.

Lambdy, funkčné rozhrania a najlepšie postupy práce s nimi sú všeobecne popísané v článku „Výrazy lambda a funkčné rozhrania: tipy a najlepšie postupy“. Táto príručka sa zameriava na niektoré konkrétne funkčné rozhrania, ktoré sa nachádzajú v java.util.funkcia balíček.

3. Funkčné rozhrania

Všetky funkčné rozhrania sa odporúčajú mať informatívne @Funkčné rozhranie anotácia. Toto nielen jasne komunikuje účel tohto rozhrania, ale umožňuje to aj kompilátoru vygenerovať chybu, ak anotované rozhranie nespĺňa podmienky.

Akékoľvek rozhranie so SAM (Single Abstract Method) je funkčné rozhraniea jeho implementácia sa môže považovať za výraz lambda.

Upozorňujeme, že Java 8 predvolené metódy nie sú abstraktné a nepočítajú sa: funkčné rozhranie môže mať stále viac predvolené metódy. Môžete to pozorovať pri pohľade na Funkcie dokumentácia.

4. Funkcie

Najjednoduchším a najbežnejším prípadom lambdy je funkčné rozhranie s metódou, ktorá prijíma jednu hodnotu a vracia inú. Túto funkciu jedného argumentu predstavuje znak Funkcia rozhranie, ktoré je parametrizované typmi jeho argumentu a návratovou hodnotou:

verejné rozhranie Funkcia {…}

Jedným z spôsobov použitia Funkcia typu v štandardnej knižnici je Map.computeIfAbsent metóda, ktorá vracia hodnotu z mapy pomocou kľúča, ale počíta hodnotu, ak kľúč ešte nie je na mape. Na výpočet hodnoty používa odovzdanú implementáciu funkcie:

Názov mapy = nová HashMap (); Hodnota celého čísla = nameMap.computeIfAbsent ("John", s -> s.length ());

Hodnota, v tomto prípade, sa vypočíta uplatnením funkcie na kľúč, vložením do mapy a tiež vrátením z volania metódy. Mimochodom, môžeme nahradiť lambdu odkazom na metódu, ktorá sa zhoduje s typmi odovzdaných a vrátených hodnôt.

Pamätajte, že objekt, na ktorý je metóda vyvolaná, je v skutočnosti implicitným prvým argumentom metódy, ktorá umožňuje vrhnúť inštančnú metódu dĺžka odkaz na a Funkcia rozhranie:

Hodnota celého čísla = nameMap.computeIfAbsent ("John", reťazec :: dĺžka);

The Funkcia rozhranie má tiež predvolené komponovať metóda, ktorá umožňuje kombinovať niekoľko funkcií do jednej a vykonávať ich postupne:

Funkcia intToString = Object :: toString; Citát funkcie = s -> "'" + s + "'"; Funkcia quoteIntToString = quote.compose (intToString); assertEquals ("'5'", quoteIntToString.apply (5));

The quoteIntToString Funkcia je kombináciou citovať funkcia použitá na výsledok intToString funkcie.

5. Špecializácie primitívnych funkcií

Pretože primitívny typ nemôže byť argumentom generického typu, existujú verzie Funkcia rozhranie pre najpoužívanejšie primitívne typy dvojitý, int, dlhoa ich kombinácie v typoch argumentov a návratov:

  • IntFunction, LongFunction, DoubleFunction: argumenty sú zadaného typu, návratový typ je parametrizovaný
  • ToIntFunction, ToLongFunction, ToDoubleFunction: návratový typ je zadaného typu, argumenty sú parametrizované
  • DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction - s argumentom aj návratovým typom definovaným ako primitívne typy, špecifikované ich menami

Neexistuje žiadne vopred pripravené funkčné rozhranie napríklad pre funkciu, ktorá má a krátky a vráti a bajt, ale nič vám nebráni v tom, aby ste napísali svoj vlastný:

@ Verejné rozhranie @FunctionalInterface ShortToByteFunction {byte applyAsByte (short s); }

Teraz môžeme napísať metódu, ktorá transformuje pole krátky na pole bajt pomocou pravidla definovaného a ShortToByteFunction:

public byte [] transformArray (short [] pole, funkcia ShortToByteFunction) {byte [] transformedArray = nový bajt [array.length]; for (int i = 0; i <array.length; i ++) {transformedArray [i] = function.applyAsByte (pole [i]); } návrat transformedArray; }

Tu je príklad, ako by sme ho mohli použiť na transformáciu poľa šortiek na pole bajtov vynásobené 2:

short [] pole = {(short) 1, (short) 2, (short) 3}; byte [] transformedArray = transformArray (pole, s -> (byte) (s * 2)); byte [] expectArray = {(byte) 2, (byte) 4, (byte) 6}; assertArrayEquals (expectArray, transformedArray);

6. Špecializácie na funkciu dvoch arít

Na definovanie lambdas dvoma argumentmi musíme použiť ďalšie rozhrania, ktoré obsahujú „Bi ” kľúčové slovo v ich názvoch: BiFunkcia, ToDoubleBiFunction, ToIntBiFunctiona ToLongBiFunction.

BiFunkcia má generované argumenty aj návratový typ, zatiaľ čo ToDoubleBiFunction a ďalšie vám umožňujú vrátiť primitívnu hodnotu.

Jeden z typických príkladov použitia tohto rozhrania v štandardnom API je v Mapa.miestoVšetko metóda, ktorá umožňuje nahradiť všetky hodnoty v mape nejakou vypočítanou hodnotou.

Použime a BiFunkcia implementácia, ktorá dostane kľúč a starú hodnotu na výpočet novej hodnoty platu a jej vrátenie.

Platy na mape = nová HashMap (); platy.put ("John", 40000); platy.put ("Freddy", 30000); platy.put ("Samuel", 50000); platries.replaceAll ((name, oldValue) -> name.equals ("Freddy")? oldValue: oldValue + 10 000);

7. Dodávatelia

The Dodávateľ funkčné rozhranie je ďalším Funkcia špecializácia, ktorá neberie žiadne argumenty. Spravidla sa používa na lenivé generovanie hodnôt. Napríklad definujme funkciu, ktorá bude druhá mocnina a dvojitý hodnotu. Nebude dostávať samotnú hodnotu, ale a Dodávateľ tejto hodnoty:

public double squareLazy (Supplier lazyValue) {return Math.pow (lazyValue.get (), 2); }

To nám umožňuje lenivo vygenerovať argument na vyvolanie tejto funkcie pomocou a Dodávateľ implementácia. To môže byť užitočné, ak generovanie tohto argumentu trvá pomerne dlho. Budeme to simulovať pomocou Guavy spať neprerušovane metóda:

Dodávateľ lazyValue = () -> {Uninterruptibles.sleepUninterruptibly (1000, TimeUnit.MILLISECONDS); návrat 9d; }; Double valueSquared = squareLazy (lazyValue);

Ďalším prípadom použitia pre dodávateľa je definovanie logiky pre generovanie sekvencie. Na ukážku použijeme statiku Stream.generate metóda na vytvorenie a Prúd z Fibonacciho čísel:

int [] fibs = {0, 1}; Stream fibonacci = Stream.generate (() -> {int result = fibs [1]; int fib3 = fibs [0] + fibs [1]; fibs [0] = fibs [1]; fibs [1] = fib3; návrat výsledok;});

Funkcia, ktorá sa odovzdáva Stream.generate metóda implementuje Dodávateľ funkčné rozhranie. Všimnite si, že aby bolo užitočné ako generátor, Dodávateľ obvykle potrebuje akýsi externý stav. V tomto prípade sa jeho stav skladá z dvoch posledných Fibonacciho poradových čísel.

Na implementáciu tohto stavu používame pole namiesto niekoľkých premenných, pretože všetky externé premenné použité vo vnútri lambda musia byť efektívne konečné.

Ostatné špecializácie Dodávateľ funkčné rozhranie patrí BooleanDodávateľ, DoubleSupplier, LongSupplier a IntSupplier, ktorých návratové typy sú zodpovedajúce primitívy.

8. Spotrebitelia

Na rozdiel od Dodávateľ, Spotrebiteľ prijme generifikovaný argument a nič nevráti. Je to funkcia, ktorá predstavuje vedľajšie účinky.

Pozdravme napríklad všetkých v zozname mien vytlačením pozdravu v konzole. Lambda prešla do List.forEach metóda implementuje Spotrebiteľ funkčné rozhranie:

Názvy zoznamu = Arrays.asList ("John", "Freddy", "Samuel"); names.forEach (name -> System.out.println ("Dobrý deň," + meno));

Existujú aj špecializované verzie servera SpotrebiteľDoubleConsumer, IntConsumer a LongConsumer - ktoré dostávajú primitívne hodnoty ako argumenty. Zaujímavejšie je BiConsumer rozhranie. Jedným z jeho prípadov použitia je iterácia cez záznamy mapy:

Vek mapy = nový HashMap (); age.put ("John", 25); age.put ("Freddy", 24); age.put ("Samuel", 30); age.forEach ((meno, vek) -> System.out.println (meno + "je" + vek + "rokov starý"));

Ďalšia sada špecializovaných BiConsumer verzie sa skladá z ObjDoubleConsumer, ObjIntConsumera ObjLongConsumer ktoré dostávajú dva argumenty, z ktorých jeden je generovaný a druhý je primitívny typ.

9. Predikáty

V matematickej logike je predikát funkcia, ktorá prijíma hodnotu a vracia boolovskú hodnotu.

The Predikát funkčné rozhranie je špecializácia a Funkcia ktorý prijme generovanú hodnotu a vráti logickú hodnotu. Typický prípad použitia Predikát lambda má filtrovať zbierku hodnôt:

Názvy zoznamu = Arrays.asList ("Angela", "Aaron", "Bob", "Claire", "David"); Zoznam namesWithA = names.stream () .filter (name -> name.startsWith ("A")) .collect (Collectors.toList ());

V kóde vyššie filtrujeme zoznam pomocou Prúd API a ponechajte si iba mená, ktoré sa začínajú písmenom „A“. Logika filtrovania je obsiahnutá v Predikát implementácia.

Rovnako ako vo všetkých predchádzajúcich príkladoch existujú IntPredicate, DoublePredicate a LongPredicate verzie tejto funkcie, ktoré dostávajú primitívne hodnoty.

10. Prevádzkovatelia

Prevádzkovateľ rozhrania sú špeciálne prípady funkcie, ktorá prijíma a vracia rovnaký typ hodnoty. The UnaryOperator rozhranie prijme jediný argument. Jedným z jeho prípadov použitia v API Collection je nahradenie všetkých hodnôt v zozname niektorými vypočítanými hodnotami rovnakého typu:

Názvy zoznamu = Arrays.asList ("bob", "josh", "megan"); names.replaceAll (name -> name.toUpperCase ());

The List.replaceAll funkcia sa vráti neplatný, pretože nahrádza platné hodnoty. Z dôvodu účelu musí lambda použitá na transformáciu hodnôt zoznamu vrátiť rovnaký typ výsledku, aký prijíma. To je dôvod, prečo UnaryOperator je tu užitočné.

Samozrejme, namiesto meno -> meno.toUpperCase (), môžete jednoducho použiť odkaz na metódu:

names.replaceAll (String :: toUpperCase);

Jedným z najzaujímavejších prípadov použitia a BinaryOperator je redukčná operácia. Predpokladajme, že chceme agregovať kolekciu celých čísel do súčtu všetkých hodnôt. S Prúd API, mohli by sme to urobiť pomocou kolektora, ale všeobecnejším spôsobom, ako to urobiť, je použitie súboru zmenšiť metóda:

Zoznam hodnôt = Arrays.asList (3, 5, 8, 9, 12); int sum = values.stream () .reduce (0, (i1, i2) -> i1 + i2); 

The zmenšiť metóda prijíma počiatočnú hodnotu akumulátora a BinaryOperator funkcie. Argumenty tejto funkcie sú dvojicou hodnôt rovnakého typu a samotná funkcia obsahuje logiku ich spojenia v jednej hodnote rovnakého typu. Úspešná funkcia musí byť asociatívna, čo znamená, že nezáleží na poradí agregácie hodnôt, t. j. by mala platiť nasledujúca podmienka:

op.apply (a, op.apply (b, c)) == op.apply (op.apply (a, b), c)

Asociačný majetok a BinaryOperator operátorská funkcia umožňuje ľahko paralelizovať proces redukcie.

Samozrejme, existujú aj špecializácie UnaryOperator a BinaryOperator ktoré možno použiť s primitívnymi hodnotami, a to DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperatora LongBinaryOperator.

11. Staršie funkčné rozhrania

Nie všetky funkčné rozhrania sa objavili v prostredí Java 8. Mnoho rozhraní z predchádzajúcich verzií Java vyhovuje obmedzeniam a Funkčné rozhranie a môžu sa použiť ako lambdy. Výrazným príkladom je Spustiteľné a Vyvolávateľná rozhrania, ktoré sa používajú v súbežných API. V prostredí Java 8 sú tieto rozhrania tiež označené znakom @Funkčné rozhranie anotácia. To nám umožňuje výrazne zjednodušiť kód súbežnosti:

Vlákno vlákna = nové vlákno (() -> System.out.println ("Ahoj z iného vlákna")); thread.start ();

12. Záver

V tomto článku sme opísali rôzne funkčné rozhrania prítomné v rozhraní Java 8 API, ktoré možno použiť ako výrazy lambda. Zdrojový kód článku je k dispozícii na GitHub.