Funkcionalna sučelja u Javi 8

1. Uvod

Ovaj je članak vodič za različita funkcionalna sučelja prisutna u Javi 8, njihove općenite slučajeve uporabe i upotrebu u standardnoj JDK knjižnici.

2. Lambde u Javi 8

Java 8 donijela je snažno novo sintaktičko poboljšanje u obliku lambda izraza. Lambda je anonimna funkcija s kojom se može postupati kao s prvorazrednim građaninom jezika, na primjer prenijeti ili vratiti iz metode.

Prije Jave 8, obično biste kreirali klasu za svaki slučaj u kojem je trebalo enkapsulirati jedan dio funkcionalnosti. To je podrazumijevalo puno nepotrebnog koda za definiranje nečega što je služilo kao primitivni prikaz funkcije.

Lambda-e, funkcionalna sučelja i općenito najbolji postupci s njima opisani su u članku "Lambda izrazi i funkcionalna sučelja: savjeti i najbolji primjeri". Ovaj se vodič fokusira na neka određena funkcionalna sučelja koja su prisutna u java.util.funkcija paket.

3. Funkcionalna sučelja

Sva funkcionalna sučelja preporučuju se informativnog karaktera @FunctionalInterface bilješka. To ne samo da jasno komunicira svrhu ovog sučelja, već omogućuje i kompajleru da generira pogrešku ako komentirano sučelje ne udovoljava uvjetima.

Bilo koje sučelje sa SAM-om (pojedinačna apstraktna metoda) funkcionalno je sučelje, a njegova provedba može se tretirati kao lambda izrazi.

Imajte na umu da Java 8 zadano metode nisu sažetak i ne računaju se: funkcionalno sučelje i dalje može imati višestruko zadano metode. To možete promatrati gledajući Funkcije dokumentacija.

4. Funkcije

Najjednostavniji i najopćenitiji slučaj lambda je funkcionalno sučelje s metodom koja prima jednu vrijednost, a vraća drugu. Ovu funkciju pojedinog argumenta predstavlja Funkcija sučelje koje je parametrizirano vrstama svog argumenta i povratnom vrijednošću:

funkcija javnog sučelja {…}

Jedna od upotreba Funkcija tip u standardnoj knjižnici je Map.computeIfAbsent metoda koja vraća vrijednost s karte ključem, ali izračunava vrijednost ako ključ već nije prisutan na karti. Da bi izračunao vrijednost, koristi proslijeđenu implementaciju funkcije:

MapMap = nova HashMap (); Cijela vrijednost = nameMap.computeIfAbsent ("John", s -> s.length ());

Vrijednost će se u ovom slučaju izračunati primjenom funkcije na ključ, staviti unutar mape i također se vratiti iz poziva metode. Usput, možemo zamijeniti lambda referencom metode koja odgovara prosljeđenim i vraćenim vrstama vrijednosti.

Zapamtite da je objekt na koji se metoda poziva zapravo implicitni prvi argument metode, koji omogućuje lijevanje instance instance duljina referenca na a Funkcija sučelje:

Cijela vrijednost = nameMap.computeIfAbsent ("John", String :: length);

The Funkcija sučelje također ima zadano sastaviti metoda koja omogućuje kombiniranje nekoliko funkcija u jednu i njihovo izvršavanje uzastopno:

Funkcija intToString = Object :: toString; Citat funkcije = s -> "'" + s + "'"; Funkcija quoteIntToString = quote.compose (intToString); assertEquals ("'5" ", quoteIntToString.apply (5));

The quoteIntToString funkcija je kombinacija citat funkcija primijenjena na rezultat intToString funkcija.

5. Specijalizacije za primitivne funkcije

Budući da primitivni tip ne može biti generički argument tipa, postoje verzije Funkcija sučelje za najčešće korištene primitivne tipove dvostruko, int, dugo, i njihove kombinacije u vrstama argumenata i povratka:

  • IntFunction, LongFunction, DoubleFunction: argumenti su određenog tipa, povratni je tip parametriziran
  • ToIntFunction, ToLongFunction, ToDoubleFunction: povratni tip je navedenog tipa, argumenti su parametrizirani
  • DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction - imaju i argument i tip povratka definirane kao primitivni tipovi, kako je određeno njihovim imenima

Nema gotovog funkcionalnog sučelja za, recimo, funkciju koja zahtijeva kratak i vraća a bajt, ali ništa vas ne sprečava da napišete svoje:

@FunctionalInterface javno sučelje ShortToByteFunction {bajt applyAsByte (kratki s); }

Sada možemo napisati metodu koja transformira niz od kratak u niz od bajt koristeći pravilo definirano a ShortToByteFunction:

javni bajt [] transformArray (kratki [] niz, funkcija ShortToByteFunction) {bajt [] transformedArray = novi bajt [niz.duljina]; za (int i = 0; i <niz.duljina; i ++) {transformedArray [i] = function.applyAsByte (niz [i]); } povratak transformedArray; }

Evo kako bismo ga mogli koristiti za transformiranje niza kratkih spojeva u niz bajtova pomnoženih s 2:

kratki [] niz = {(kratki) 1, (kratki) 2, (kratki) 3}; bajt [] transformedArray = transformArray (niz, s -> (bajt) (s * 2)); bajt [] očekuje seArray = {(bajt) 2, (bajt) 4, (bajt) 6}; assertArrayEquals (očekivaniArray, transformiraniArray);

6. Specijalizacije za funkcije s dvije arterije

Da bismo definirali lambde s dva argumenta, moramo koristiti dodatna sučelja koja sadrže "Dvo" ključna riječ u njihovim imenima: BiFunction, ToDoubleBiFunction, ToIntBiFunction, i ToLongBiFunction.

BiFunction ima generirani i argumente i tip povratka, dok ToDoubleBiFunction a drugi vam omogućuju vraćanje primitivne vrijednosti.

Jedan od tipičnih primjera korištenja ovog sučelja u standardnom API-ju je Map.replaceAll metoda koja omogućuje zamjenu svih vrijednosti na karti nekom izračunatom vrijednošću.

Koristimo a BiFunction implementacija koja prima ključ i staru vrijednost za izračunavanje nove vrijednosti plaće i vraćanje iste.

Mape plaće = novi HashMap (); plaće.put ("Ivan", 40000); plaće.put ("Freddy", 30000); plaće.put ("Samuel", 50000); pays.replaceAll ((name, oldValue) -> name.equals ("Freddy")? oldValue: oldValue + 10000);

7. Dobavljači

The Dobavljač funkcionalno sučelje je još jedno Funkcija specijalizacija koja ne uzima nikakve argumente. Tipično se koristi za lijeno generiranje vrijednosti. Na primjer, definirajmo funkciju koja kvadrira a dvostruko vrijednost. Dobit će ne samu vrijednost, već a Dobavljač ove vrijednosti:

javni dvostruki squareLazy (dobavljač lazyValue) {return Math.pow (lazyValue.get (), 2); }

To nam omogućuje lijeno generiranje argumenta za pozivanje ove funkcije pomoću a Dobavljač provedba. To može biti korisno ako generiranje ovog argumenta oduzima znatno vrijeme. To ćemo simulirati pomoću Guave spavajNeprekidno metoda:

Dobavljač lazyValue = () -> {Uninterruptibles.sleepUninterruptibly (1000, TimeUnit.MILLISECONDS); povratak 9d; }; Double valueSquared = squareLazy (lazyValue);

Sljedeći slučaj upotrebe za dobavljača je definiranje logike za generiranje sekvenci. Da bismo je demonstrirali, upotrijebimo statiku Stream.generirati metoda za stvaranje a Stream Fibonaccijevih brojeva:

int [] fibs = {0, 1}; Stream fibonacci = Stream.generate (() -> {int rezultat = fibs [1]; int fib3 = fibs [0] + fibs [1]; fibs [0] = fibs [1]; fibs [1] = fib3; vratiti rezultat;});

Funkcija koja se prenosi na Stream.generirati metoda provodi Dobavljač funkcionalno sučelje. Primijetite da će biti koristan kao generator, Dobavljač obično treba nekakvo vanjsko stanje. U ovom se slučaju njegovo stanje sastoji od dva zadnja Fibonaccijeva broja sekvenci.

Da bismo implementirali ovo stanje, koristimo niz umjesto nekoliko varijabli, jer sve vanjske varijable korištene unutar lambde moraju biti efektivno konačne.

Ostale specijalizacije za Dobavljač funkcionalno sučelje uključuju BooleanSupplier, DoubleSupplier, LongSupplier i IntSupplier, čiji su povratni tipovi odgovarajući primitivi.

8. Potrošači

Za razliku od Dobavljač, Potrošač prihvaća generirani argument i ne vraća ništa. To je funkcija koja predstavlja nuspojave.

Na primjer, pozdravimo sve na popisu imena ispisujući pozdrav na konzoli. Lambda je prešla na Popis.za svakog metoda provodi Potrošač funkcionalno sučelje:

Imena popisa = Arrays.asList ("John", "Freddy", "Samuel"); names.forEach (name -> System.out.println ("Zdravo," + ime));

Postoje i specijalizirane verzije PotrošačDoubleConsumer, IntConsumer i LongConsumer - koji primaju primitivne vrijednosti kao argumente. Zanimljiviji je BiConsumer sučelje. Jedan od slučajeva upotrebe je itiriranje kroz unose karte:

Dob karte = novi HashMap (); age.put ("Ivan", 25); age.put ("Freddy", 24); age.put ("Samuel", 30); age.forEach ((ime, dob) -> System.out.println (ime + "je" + dob + "godine"));

Još jedan set specijaliziranih BiConsumer inačica sastoji se od ObjDoubleConsumer, ObjIntConsumer, i ObjLongConsumer koji primaju dva argumenta od kojih je jedan generiran, a drugi primitivan tip.

9. Predikati

U matematičkoj logici predikat je funkcija koja prima vrijednost i vraća logičku vrijednost.

The Predikat funkcionalno sučelje je specijalizacija a Funkcija koja prima generiranu vrijednost i vraća logičku vrijednost. Tipičan slučaj upotrebe Predikat lambda je filtriranje zbirke vrijednosti:

Imena popisa = Arrays.asList ("Angela", "Aaron", "Bob", "Claire", "David"); Popis namesWithA = names.stream () .filter (name -> name.startsWith ("A")) .collect (Collectors.toList ());

U gornjem kodu filtriramo popis pomoću Stream API i zadržavaju samo imena koja počinju slovom "A". Logika filtriranja sadržana je u Predikat provedba.

Kao i u svim prethodnim primjerima, postoje Nepredvidljivo, DoublePredicate i LongPredicate inačice ove funkcije koje primaju primitivne vrijednosti.

10. Operateri

Operater sučelja su posebni slučajevi funkcije koja prima i vraća isti tip vrijednosti. The UnaryOperator sučelje prima jedan argument. Jedan od njegovih slučajeva upotrebe u API-ju Collections je zamjena svih vrijednosti na popisu nekim izračunatim vrijednostima istog tipa:

Imena popisa = Arrays.asList ("bob", "josh", "megan"); names.replaceAll (name -> name.toUpperCase ());

The List.replaceAll funkcija se vraća poništiti, jer zamjenjuje postojeće vrijednosti. Da bi odgovarao svrsi, lambda koja se koristi za transformiranje vrijednosti popisa mora vratiti isti tip rezultata kao što prima. To je razlog zašto UnaryOperator je ovdje korisno.

Naravno, umjesto name -> name.toUpperCase (), možete jednostavno upotrijebiti referencu metode:

names.replaceAll (String :: toUpperCase);

Jedan od najzanimljivijih slučajeva upotrebe a BinaryOperator je operacija smanjenja. Pretpostavimo da želimo prikupiti zbirku cijelih brojeva u zbroju svih vrijednosti. S Stream API, to bismo mogli učiniti pomoću kolektora, ali općenitiji način za to bio bi korištenje smanjiti metoda:

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

The smanjiti metoda prima početnu vrijednost akumulatora i a BinaryOperator funkcija. Argumenti ove funkcije su par vrijednosti istog tipa, a sama funkcija sadrži logiku njihovog spajanja u jednu vrijednost istog tipa. Položena funkcija mora biti asocijativna, što znači da redoslijed agregiranja vrijednosti nije važan, tj. trebao bi vrijediti sljedeći uvjet:

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

Asocijativno svojstvo a BinaryOperator Operatorska funkcija omogućuje lako paraleliziranje procesa smanjenja.

Naravno, postoje i specijalizacije za UnaryOperator i BinaryOperator koja se može koristiti s primitivnim vrijednostima, naime DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator, i LongBinaryOperator.

11. Naslijeđena funkcionalna sučelja

Nisu se sva funkcionalna sučelja pojavila u Javi 8. Mnoga sučelja iz prethodnih verzija Jave sukladna su ograničenjima a Funkcionalno sučelje a mogu se koristiti i kao lambda. Istaknuti primjer je Izvodljivo i Pozivno sučelja koja se koriste u istodobnim API-ima. U Javi 8 ta su sučelja također označena s @FunctionalInterface bilješka. To nam omogućuje da znatno pojednostavimo paralelni kod:

Nit niti = nova nit (() -> System.out.println ("Pozdrav iz druge niti")); nit.start ();

12. Zaključak

U ovom smo članku opisali različita funkcionalna sučelja prisutna u Java 8 API-ju koja se mogu koristiti kao lambda izrazi. Izvorni kôd članka dostupan je na GitHubu.