Lambda izrazi i funkcionalna sučelja: Savjeti i najbolji primjeri iz prakse

1. Pregled

Sad kad je Java 8 postala široko korištena, počeli su se pojavljivati ​​obrasci i najbolje prakse za neke od njezinih glavnih značajki. U ovom uputstvu pobliže ćemo pogledati funkcionalna sučelja i lambda izraze.

2. Dajte prednost standardnim funkcionalnim sučeljima

Funkcionalna sučelja koja su prikupljena u java.util.funkcija paket, zadovoljava potrebe većine programera u pružanju ciljnih vrsta za lambda izraze i reference metoda. Svako od ovih sučelja općenito je i apstraktno, što ih čini jednostavnim za prilagodbu gotovo svim lambda izrazima. Programeri bi trebali istražiti ovaj paket prije stvaranja novih funkcionalnih sučelja.

Razmislite o sučelju Foo:

@FunctionalInterface javno sučelje Foo {Metoda niza (niz niza); }

i metoda dodati() u nekom razredu UseFoo, koji uzima ovo sučelje kao parametar:

javni String add (String string, Foo foo) {return foo.method (string); }

Da biste je izvršili, napisali biste:

Foo foo = parametar -> parametar + "od lambda"; Rezultat niza = useFoo.add ("Poruka", foo);

Pogledajte bliže i vidjet ćete to Foo nije ništa više od funkcije koja prihvaća jedan argument i daje rezultat. Java 8 već nudi takvo sučelje u sustavu Funkcija iz paketa funkcije java.util.

Sada možemo ukloniti sučelje Foo u potpunosti i promijenite naš kôd u:

javni String add (String string, Funkcija fn) {return fn.apply (string); }

Da bismo to izvršili, možemo napisati:

Funkcija fn = parametar -> parametar + "od lambda"; Rezultat niza = useFoo.add ("Poruka", fn);

3. Koristite @FunctionalInterface Bilješka

Označite svoje funkcionalno sučelje s @FunctionalInterface. U početku se čini da je ova napomena beskorisna. Čak i bez njega, vaše će se sučelje tretirati kao funkcionalno sve dok ima samo jednu apstraktnu metodu.

Ali zamislite veliki projekt s nekoliko sučelja - teško je sve kontrolirati ručno. Sučelje, koje je dizajnirano da bude funkcionalno, moglo bi se slučajno promijeniti dodavanjem drugih apstraktnih metoda / metoda, čineći ga neupotrebljivim kao funkcionalno sučelje.

Ali pomoću @FunctionalInterface napomena, kompajler će pokrenuti pogrešku kao odgovor na svaki pokušaj razbijanja unaprijed definirane strukture funkcionalnog sučelja. Također je vrlo praktičan alat za olakšavanje razumijevanja arhitekture vaših aplikacija drugim programerima.

Dakle, upotrijebite ovo:

@FunctionalInterface javno sučelje Foo {String metoda (); }

umjesto samo:

javno sučelje Foo {String metoda (); }

4. Ne prekomjerno upotrebljavajte zadane metode u funkcionalnim sučeljima

Funkcionalnom sučelju možemo jednostavno dodati zadane metode. To je prihvatljivo za ugovor o funkcionalnom sučelju sve dok postoji samo jedna apstraktna deklaracija metode:

@FunctionalInterface javno sučelje Foo {Metoda niza (niz niza); zadana praznina defaultMethod () {}}

Funkcionalna sučelja mogu se proširiti drugim funkcionalnim sučeljima ako njihove apstraktne metode imaju isti potpis.

Na primjer:

@FunctionalInterface javno sučelje FooExtended proširuje Baz, Bar {} @FunctionalInterface javno sučelje Baz {Metoda niza (string niza); zadani niz defaultBaz () {}} Traka javnog sučelja @FunctionalInterface {Metoda niza (niz niza); zadani niz defaultBar () {}}

Kao i kod uobičajenih sučelja, Proširivanje različitih funkcionalnih sučelja istom zadanom metodom može biti problematično.

Na primjer, dodajmo defaultCommon () metoda za Bar i Baz sučelja:

@FunctionalInterface javno sučelje Baz {Metoda niza (niz niza); zadani niz defaultBaz () {} zadani niz defaultCommon () {}} Traka javnog sučelja @FunctionalInterface {Metoda niza (niz niza); zadani niz defaultBar () {} zadani niz defaultCommon () {}}

U ovom ćemo slučaju dobiti pogrešku vremena kompajliranja:

sučelje FooExtended nasljeđuje nepovezane zadane vrijednosti za defaultCommon () od tipova Baz i Bar ...

Da biste to popravili, defaultCommon () metodu treba nadjačati u FooExtended sučelje. Možemo, naravno, osigurati prilagođenu provedbu ove metode. Međutim, također možemo ponovno koristiti implementaciju iz nadređenog sučelja:

@FunctionalInterface javno sučelje FooExtended proširuje Baz, Bar {@Override default String defaultCommon () {return Bar.super.defaultCommon (); }}

Ali moramo biti oprezni. Dodavanje previše zadanih metoda sučelju nije baš dobra arhitektonska odluka. To bi trebalo smatrati kompromisom, koji se koristi po potrebi samo za nadogradnju postojećih sučelja bez narušavanja unatrag kompatibilnosti.

5. Instancirajte funkcionalna sučelja s Lambda izrazima

Kompajler će vam omogućiti upotrebu unutarnje klase za instanciranje funkcionalnog sučelja. Međutim, to može dovesti do vrlo opširnog koda. Trebali biste preferirati lambda izraze:

Foo foo = parametar -> parametar + "od Foo";

preko unutarnjeg razreda:

Foo fooByIC = novi Foo () {@Preuzmi javnu metodu niza (niz niza) {return string + "from Foo"; }}; 

Pristup lambda izraza može se koristiti za bilo koje prikladno sučelje iz starih knjižnica. Koristan je za sučelja poput Izvodljivo, Usporednik, i tako dalje. Međutim, ovo ne znači da biste trebali pregledati cijelu svoju stariju bazu kodova i promijeniti sve.

6. Izbjegavajte metode preopterećenja s funkcijskim sučeljima kao parametrima

Koristite metode s različitim imenima kako biste izbjegli sudare; pogledajmo primjer:

javno sučelje Procesor {String postupak (poziva se c) baca iznimku; String postupak (dobavljači); } javna klasa ProcessorImpl implementira procesor {@Override javni nizni proces (poziva se c) baca izuzetak {// detalji implementacije} @Override javni string proces (dobavljači) {// detalji implementacije}}

Na prvi pogled to se čini razumnim. Ali svaki pokušaj izvršenja bilo kojeg od ProcesorImplS metode:

Rezultat niza = processor.process (() -> "abc");

završava pogreškom sa sljedećom porukom:

referenca na proces dvosmislena je i za postupak metode (java.util.concurrent.Callable) u com.baeldung.java8.lambda.tips.ProcessorImpl i za postupak metode (java.util.function.Supplier) u com.baeldung.java8.lambda. savjeti.ProcesorImpl match

Da bismo riješili ovaj problem, imamo dvije mogućnosti. Prvo je korištenje metoda s različitim imenima:

String processWithCallable (Callable c) baca iznimku; String processWithSupplier (dobavljači);

Druga je ručno izvođenje lijevanja. Ovo nije poželjno.

Rezultat niza = processor.process ((dobavljač) () -> "abc");

7. Ne tretirajte Lambda izraze kao unutarnju nastavu

Unatoč našem prethodnom primjeru, gdje smo unutarnju klasu u osnovi zamijenili lambda izrazom, dva su pojma na važan način različita: opseg.

Kada koristite unutarnju klasu, ona stvara novi opseg. Lokalne varijable možete sakriti iz opsega koji obuhvaća instanciranjem novih lokalnih varijabli s istim imenima. Možete koristiti i ključnu riječ ovaj unutar vaše unutarnje klase kao referenca na njezinu instancu.

Međutim, lambda izrazi rade s obuhvaćajućim opsegom. Ne možete sakriti varijable iz opsega koji se nalazi unutar tijela lambde. U ovom slučaju, ključna riječ ovaj je referenca na priloženu instancu.

Na primjer, u razredu UseFoo imate varijablu instance vrijednost:

private String value = "Uključivanje vrijednosti opsega";

Zatim u neku metodu ove klase stavite sljedeći kod i izvršite ovu metodu.

javni String scopeExperiment () {Foo fooIC = new Foo () {Vrijednost niza = "Vrijednost unutarnje klase"; @Override public String method (string string) {return this.value; }}; Rezultat nizaIC = fooIC.method (""); Foo fooLambda = parametar -> {Vrijednost niza = "Lambda vrijednost"; vrati this.value; }; Niz rezultataLambda = fooLambda.method (""); return "Rezultati: resultIC =" + resultIC + ", resultLambda =" + resultLambda; }

Ako izvršite scopeExperiment () metodom dobit ćete sljedeći rezultat: Rezultati: resultIC = Unutarnja vrijednost klase, resultLambda = Uključivanje vrijednosti opsega

Kao što vidite, pozivom ovo.vrijednost u IC-u možete pristupiti lokalnoj varijabli iz njezine instance. Ali u slučaju lambde, ovo.vrijednost poziv vam daje pristup varijabli vrijednost koja je definirana u UseFoo klase, ali ne i na varijablu vrijednost definirana unutar tijela lambde.

8. Lambda izrazi neka budu kratki i neobjašnjivi

Ako je moguće, upotrijebite konstrukcije u jednom retku umjesto velikog bloka koda. Zapamtiti lambda bi trebala bitiizraz, a ne narativ. Unatoč svojoj sažetoj sintaksi, lambda bi trebala precizno izraziti funkcionalnost koju pružaju.

Ovo je uglavnom stilistički savjet, jer se izvedba neće drastično promijeniti. Međutim, općenito je puno lakše razumjeti i raditi s takvim kodom.

To se može postići na mnogo načina - pogledajmo izbliza.

8.1. Izbjegavajte blokove koda u Lambdinom tijelu

U idealnoj situaciji, lambde bi trebale biti napisane u jednom retku koda. Ovim pristupom lambda je samorazumljiva konstrukcija koja deklarira koju akciju treba izvršiti s kojim podacima (u slučaju lambda s parametrima).

Ako imate velik blok koda, lambda funkcionalnost nije odmah jasna.

Imajući ovo na umu, učinite sljedeće:

Foo foo = parametar -> buildString (parametar);
private String buildString (parametar niza) {Rezultat niza = "Nešto" + parametar; // mnogi retci koda vraćaju rezultat; }

umjesto:

Foo foo = parametar -> {Rezultat niza = "Nešto" + parametar; // mnogi retci koda vraćaju rezultat; };

Međutim, nemojte koristiti ovo pravilo "jednoredne lambde" kao dogmu. Ako imate dva ili tri retka u lambda definiciji, možda neće biti vrijedno izdvojiti taj kôd u drugu metodu.

8.2. Izbjegavajte specificiranje vrsta parametara

Kompajler je u većini slučajeva u stanju riješiti vrstu lambda parametara pomoću zaključivanje tipa. Stoga je dodavanje tipa parametrima neobavezno i ​​može se izostaviti.

Napravi to:

(a, b) -> a.toLowerCase () + b.toLowerCase ();

umjesto ovoga:

(Niz a, Niz b) -> a.toLowerCase () + b.toLowerCase ();

8.3. Izbjegavajte zagrade oko jednog parametra

Lambda sintaksa zahtijeva zagrade samo oko više od jednog parametra ili kada parametar uopće ne postoji. Zbog toga je sigurno vaš kod učiniti malo kraćim i izuzeti zagrade kad postoji samo jedan parametar.

Dakle, učinite ovo:

a -> a.toLowerCase ();

umjesto ovoga:

(a) -> a.toLowerCase ();

8.4. Izbjegavajte Izjavu o vraćanju i zagrade

Braces i povratak izjave su neobavezne u lambda tijelima s jednim retkom. To znači da ih se zbog jasnosti i sažetosti može izostaviti.

Napravi to:

a -> a.toLowerCase ();

umjesto ovoga:

a -> {return a.toLowerCase ()};

8.5. Upotrijebite reference metode

Vrlo često, čak i u našim prethodnim primjerima, lambda izrazi samo pozivaju metode koje su već implementirane negdje drugdje. U ovoj je situaciji vrlo korisno koristiti drugu značajku Java 8: reference metode.

Dakle, lambda izraz:

a -> a.toLowerCase ();

može se zamijeniti sa:

String :: toLowerCase;

To nije uvijek kraće, ali čini kôd čitljivijim.

9. Upotrijebite "Učinkovito konačne" varijable

Pristup ne-konačnoj varijabli unutar lambda izraza uzrokovat će pogrešku vremena kompajliranja. Ali to ne znači da biste svaku ciljanu varijablu trebali označavati kao konačni.

Prema "efektivno konačan"Koncept, kompajler svaku varijablu tretira kao konačni, sve dok se dodjeljuje samo jednom.

Sigurno je koristiti takve varijable unutar lambda-a, jer će kompajler kontrolirati njihovo stanje i pokrenuti pogrešku u vremenu kompajliranja odmah nakon bilo kojeg pokušaja da ih promijeni.

Na primjer, sljedeći se kod neće kompajlirati:

metoda javne praznine () {String localVariable = "Lokalno"; Foo foo = parametar -> {String localVariable = parametar; return localVariable; }; }

Prevoditelj će vas obavijestiti da:

Varijabla 'localVariable' već je definirana u opsegu.

Ovaj bi pristup trebao pojednostaviti postupak izrade lambda izvođenja u niti.

10. Zaštitite varijable objekta od mutacije

Jedna od glavnih svrha lambdas je upotreba paralelnog računanja - što znači da su stvarno korisne kada je u pitanju sigurnost niti.

Paradigma "efektivno konačna" ovdje puno pomaže, ali ne u svakom slučaju. Lambde ne mogu promijeniti vrijednost predmeta iz opsega koji obuhvaća. Ali u slučaju promjenjivih varijabli objekta, stanje se može promijeniti unutar lambda izraza.

Razmotrite sljedeći kod:

int [] ukupno = novo int [1]; Izvodljivo r = () -> ukupno [0] ++; r.run ();

Ovaj je zakonik legalan, kao ukupno varijabla ostaje "efektivno konačna". No hoće li objekt na koji se referira imati isto stanje nakon izvršenja lambde? Ne!

Držite ovaj primjer kao podsjetnik kako biste izbjegli kôd koji može izazvati neočekivane mutacije.

11. Zaključak

U ovom uputstvu vidjeli smo neke najbolje prakse i zamke u lambda izrazima i funkcionalnim sučeljima Java 8. Unatoč korisnosti i snazi ​​ovih novih značajki, oni su samo alati. Svaki programer treba obratiti pažnju dok ih koristi.

Kompletna izvorni kod primjer je dostupan u ovom GitHub projektu - ovo je Maven i Eclipse projekt, tako da se može uvesti i koristiti takav kakav jest.