Lambda kifejezések és funkcionális interfészek: tippek és legjobb gyakorlatok

1. Áttekintés

Most, hogy a Java 8 elterjedt, a szokások és a bevált gyakorlatok kezdtek megjelenni egyes főbb jellemzői tekintetében. Ebben az oktatóanyagban közelebbről megvizsgáljuk a funkcionális interfészeket és a lambda kifejezéseket.

2. A standard funkcionális interfészeket részesítse előnyben

Funkcionális interfészek, amelyek a java.util.function csomag, kielégíti a legtöbb fejlesztői igényt a lambda kifejezések és módszer referenciák céltípusainak biztosításában. Ezen interfészek mindegyike általános és elvont, így szinte bármilyen lambda kifejezéshez könnyen adaptálhatók. A fejlesztőknek új funkcionális interfészek létrehozása előtt meg kell vizsgálniuk ezt a csomagot.

Vegyünk egy interfészt Foo:

@FunctionalInterface nyilvános felület Foo {String metódus (String string); }

és egy módszer add () valamilyen osztályban UseFoo, amely ezt az interfészt paraméterként veszi fel:

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

A végrehajtáshoz a következőket írja:

Foo foo = paraméter -> paraméter + "lambdától"; Karakterlánc eredménye = useFoo.add ("Üzenet", foo);

Nézz közelebbről, és meglátod Foo nem más, mint egy függvény, amely elfogad egy argumentumot és eredményt produkál. A Java 8 már rendelkezik ilyen interfésszel Funkció a java.util.function csomagból.

Most eltávolíthatjuk az interfészt Foo teljesen változtassa meg kódunkat:

public String add (String string, Fn függvény) {return fn.apply (string); }

Ennek végrehajtásához írhatunk:

Fn = paraméter -> paraméter + "lambdától" függvény; Karakterlánc eredménye = useFoo.add ("Üzenet", fn);

3. Használja a @FunctionalInterface Megjegyzés

Jegyezze fel a funkcionális interfészeit @FunctionalInterface. Eleinte úgy tűnik, hogy ez a feljegyzés haszontalan. Enélkül is a kezelőfelületed ugyanolyan működőképesnek tekinthető, ha csak egy absztrakt módszerrel rendelkezik.

De képzeljen el egy nagy, több interfésszel rendelkező projektet - nehéz mindent manuálisan irányítani. A funkcionálisnak tervezett felületet véletlenül megváltoztathatja más absztrakt módszer / módszerek hozzáadásával, ami funkcionális interfészként használhatatlanná teszi.

De a @FunctionalInterface annotációval, a fordító hibát vált ki a funkcionális interfész előre definiált szerkezetének megtörésére tett bármilyen válaszként. Ez egy nagyon hasznos eszköz arra is, hogy az alkalmazás architektúráját könnyebben érthetővé tegye más fejlesztők számára.

Tehát használja ezt:

@FunctionalInterface nyilvános felület Foo {String metódus (); }

ahelyett, hogy csak:

nyilvános felület Foo {String metódus (); }

4. Ne használja túl az alapértelmezett módszereket a funkcionális interfészekben

Könnyen hozzáadhatunk alapértelmezett módszereket a funkcionális felülethez. Ez elfogadható a funkcionális interfész-szerződés számára, amennyiben csak egy elvont módszer deklaráció létezik:

@FunctionalInterface nyilvános felület Foo {String metódus (String string); default void defaultMethod () {}}

A funkcionális interfészek kiterjeszthetők más funkcionális interfészekkel is, ha absztrakt módszereik azonos aláírással rendelkeznek.

Például:

@FunctionalInterface nyilvános felület A FooExtended kiterjeszti a Baz, Bar {} @FunctionalInterface nyilvános felület Baz {String metódus (String string); alapértelmezett karakterlánc defaultBaz () {}} @FunctionalInterface nyilvános felület sáv {String módszer (karakterlánc karakterlánc); alapértelmezett karakterlánc alapértelmezett sáv () {}}

Csakúgy, mint a szokásos interfészeknél, problémás lehet a különböző funkcionális interfészek kiterjesztése ugyanazzal az alapértelmezett módszerrel.

Például tegyük hozzá a defaultCommon () módszer a Rúd és Baz interfészek:

@FunctionalInterface nyilvános felület Baz {String metódus (String string); alapértelmezett karakterlánc defaultBaz () {} alapértelmezett karakterlánc defaultCommon () {}} @FunctionalInterface nyilvános felület sáv {String módszer (karakterlánc karakterlánc); alapértelmezett karakterlánc defaultBar () {} alapértelmezett karakterlánc defaultCommon () {}}

Ebben az esetben fordítási időbeli hibát kapunk:

A FooExtended felület a Baz és a Bar típusoktól örökölje az alapértelmezett alapértelmezéseket a defaultCommon () számára.

Ennek kijavításához defaultCommon () módszert felül kell írni a FooExtended felület. Természetesen biztosíthatjuk ennek a módszernek az egyedi megvalósítását. Azonban, a megvalósítást a szülői felületről is felhasználhatjuk:

@FunctionalInterface nyilvános felület A FooExtended kiterjeszti a Baz, Bar {@Orride default String defaultCommon () {return Bar.super.defaultCommon (); }}

De vigyáznunk kell. Túl sok alapértelmezett módszer hozzáadása az interfészhez nem túl jó építészeti döntés. Ezt kompromisszumnak kell tekinteni, csak szükség esetén használható a meglévő interfészek korszerűsítésére a visszamenőleges kompatibilitás megsértése nélkül.

5. Azonnali funkcionális interfészek a Lambda kifejezésekkel

A fordító lehetővé teszi, hogy egy belső osztályt használjon egy funkcionális felület példányosítására. Ez azonban nagyon részletes kódhoz vezethet. Előnyben kell részesítenie a lambda kifejezéseket:

Foo foo = paraméter -> paraméter + "a Foo-tól";

egy belső osztály felett:

Foo fooByIC = new Foo () {@Orride public String method (String string) {return string + "from Foo"; }}; 

A lambda expressziós megközelítés használható bármely régi interfészhez a régi könyvtárakból. Olyan interfészekhez használható, mint Futható, Összehasonlító, stb. Ez azonban nem azt jelenti, hogy át kellene tekintenie az egész régebbi kódbázist és mindent meg kell változtatnia.

6. Kerülje a módszerek túlterhelését, ha a funkcionális interfészek paraméterek

Használjon különböző nevű módszereket az ütközések elkerülése érdekében; nézzünk meg egy példát:

nyilvános felület Processzor {String folyamat (hívható c) dobja a Kivételt; Karakterlánc-folyamat (szállító); } public class ProcessorImpl implementálja a processzort {@Orride public String process (Callable c) dob kivételt {// implementáció részletei} @Orride public String folyamat (szállító) {// megvalósítás részletei}}

Első pillantásra ez ésszerűnek tűnik. De bármilyen kísérlet bármelyik végrehajtására ProcessorImplMódszerei:

Karakterlánc eredménye = processzor.folyamat (() -> "abc");

hibával zárul a következő üzenettel:

A folyamatra való hivatkozás kétértelmű, a metódusfolyamat (java.util.concurrent.Callable) a com.baeldung.java8.lambda.tips.ProcessorImpl és a method process (java.util.function.Supplier) a com.baeldung.java8.lambda fájlban. tippek.ProcessorImpl match

A probléma megoldására két lehetőségünk van. Az első a különböző nevű módszerek használata:

String processWithCallable (Callable c) dobja a Kivételt; String processWithSupplier (Szállító s);

A második az öntés manuális végrehajtása. Ezt nem preferálják.

Karakterlánc eredménye = processzor.folyamat ((Szállító) () -> "abc");

7. Ne kezelje a lambda kifejezéseket belső osztályként

Korábbi példánk ellenére, ahol a belső osztályt lényegében lambda kifejezéssel helyettesítettük, a két fogalom fontos szempontból különbözik egymástól: hatókör.

Ha belső osztályt használ, az új hatókört hoz létre. A helyi változókat elrejtheti a becsatoló hatókör elől, ha új, azonos nevű helyi változókat példázza. Használhatja a kulcsszót is ez belső osztályán belül, mint a példányára való hivatkozás.

A lambda kifejezések azonban záró hatókörrel működnek. Nem rejtheti el a változókat a lambda testének belsejében lévő körből. Ebben az esetben a kulcsszó ez hivatkozás egy záró példányra.

Például az osztályban UseFoo van egy példányváltozó érték:

private String value = "Záróköri érték";

Ezután az osztály valamelyik módszerében tegye a következő kódot, és hajtsa végre ezt a módszert.

public String scopeExperiment () {Foo fooIC = new Foo () {String value = "Belső osztály értéke"; @Orride public String method (String string) {return this.value; }}; Karakterlánc eredményeIC = fooIC.method (""); Foo fooLambda = paraméter -> {String value = "Lambda érték"; adja vissza ezt.érték; }; String resultLambda = fooLambda.method (""); return "Eredmények: resultIC =" + resultIC + ", resultLambda =" + resultLambda; }

Ha végrehajtja a scopeExperiment () módszerrel a következő eredményt kapja: Eredmények: resultIC = Belső osztály értéke, resultLambda = Zárókör értéke

Mint láthatja, hívással ezt.érték az IC-ben elérhet egy helyi változót a példányából. De a lambda esetében ezt.érték A call hozzáférést biztosít a változóhoz érték amelyet a UseFoo osztály, de a változóhoz nem érték a lambda testében meghatározott.

8. Tartsa rövid és magától értetődő a Lambda kifejezéseket

Ha lehetséges, használjon egy sor konstrukciót nagy kódblokk helyett. Emlékezik lambdas legyen egykifejezés, nem elbeszélés. Tömör szintaxisa ellenére a lambdáknak pontosan ki kell fejezniük az általuk nyújtott funkcionalitást.

Ez elsősorban stilisztikai tanács, mivel a teljesítmény nem fog drasztikusan változni. Általában azonban sokkal könnyebb megérteni és dolgozni az ilyen kóddal.

Ez sokféleképpen érhető el - nézzük meg közelebbről.

8.1. Kerülje a kódblokkokat a Lambda testében

Ideális helyzetben a lambdákat egy kódsorba kell írni. Ezzel a megközelítéssel a lambda magától értetődő konstrukció, amely deklarálja, hogy milyen műveletet milyen adatokkal kell végrehajtani (paraméterekkel rendelkező lambdák esetében).

Ha nagy a kódtömbje, akkor a lambda funkciói nem azonnal tisztázottak.

Ezt szem előtt tartva tegye a következőket:

Foo foo = paraméter -> buildString (paraméter);
private String buildString (String paraméter) {String eredmény = "Valami" + paraméter; // a kód sok sora visszatér; }

ahelyett:

Foo foo = paraméter -> {String eredmény = "Valami" + paraméter; // a kód sok sora visszatér; };

Kérjük, ne használja dogmának ezt az „egysoros lambda” szabályt. Ha két vagy három sor van a lambda definíciójában, akkor nem biztos, hogy hasznos kivonni ezt a kódot egy másik módszerbe.

8.2. Kerülje a paramétertípusok megadását

A fordító a legtöbb esetben képes megoldani a lambda paraméterek típusát típusú következtetés. Ezért egy típus hozzáadása a paraméterekhez nem kötelező és elhagyható.

Csináld ezt:

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

ehelyett:

(A karakterlánc, b karakterlánc) -> a.toLowerCase () + b.toLowerCase ();

8.3. Kerülje a zárójeleket egyetlen paraméter körül

A lambda szintaxisa csak egynél több paraméter körüli zárójelet igényel, vagy ha egyáltalán nincs paraméter. Ezért biztonságos, ha a kódot egy kicsit rövidebbé teszik, és kizárják a zárójeleket, ha csak egy paraméter van.

Tehát tedd ezt:

a -> a.toLowerCase ();

ehelyett:

(a) -> a.toLowerCase ();

8.4. Kerülje a visszatérési nyilatkozatot és a zárójelet

Kapcsos zárójel és Visszatérés utasítások választhatóak az egysoros lambda testekben. Ez azt jelenti, hogy az egyértelműség és tömörség érdekében elhagyhatók.

Csináld ezt:

a -> a.toLowerCase ();

ehelyett:

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

8.5. Használja a módszer referenciáit

Nagyon gyakran, még korábbi példáinkban is, a lambda kifejezések csak máshol már alkalmazott módszereket hívnak meg. Ebben a helyzetben nagyon hasznos egy másik Java 8 szolgáltatás használata: módszer referenciák.

Tehát a lambda kifejezés:

a -> a.toLowerCase ();

helyettesíthető a következővel:

Karakterlánc :: toLowerCase;

Ez nem mindig rövidebb, de olvashatóbbá teszi a kódot.

9. Használja a „Hatékonyan végleges” változókat

A lambda kifejezéseken belüli nem végleges változó elérése fordítási idő hibát okoz. De ez nem azt jelenti, hogy minden célváltozót meg kell jelölnie végső.

Szerint a "gyakorlatilag végleges”Koncepció szerint a fordító minden változót úgy kezel végső, amíg csak egyszer van kijelölve.

Biztonságos az ilyen változók használata a lambdas-ban, mert a fordító minden változtatási kísérlet után azonnal irányítja az állapotukat, és fordítási időbeli hibát vált ki.

Például a következő kód nem áll össze:

public void módszer () {String localVariable = "Local"; Foo foo = paraméter -> {String localVariable = paraméter; return localVariable; }; }

A fordító arról tájékoztatja Önt, hogy:

A „localVariable” változó már meg van határozva a hatókörben.

Ennek a megközelítésnek le kell egyszerűsítenie a lambda végrehajtásának szálbiztonságossá tételét.

10. Védje az objektumváltozókat a mutációtól

A lambdas egyik fő célja a párhuzamos számítástechnika - ami azt jelenti, hogy valóban hasznosak a menetbiztonság terén.

A „ténylegesen végső” paradigma itt sokat segít, de nem minden esetben. A Lambdas nem változtathatja meg az objektum értékét a hatókör bezárásával. De a mutálható objektumváltozók esetén a lambda kifejezéseken belül egy állapot megváltoztatható.

Vegye figyelembe a következő kódot:

int [] összesen = új int [1]; Futható r = () -> összesen [0] ++; r.run ();

Ez a kód törvényes, mivel teljes változó „gyakorlatilag végleges” marad. De vajon az általa hivatkozott objektumnak ugyanaz az állapota lesz-e a lambda végrehajtása után? Nem!

Tartsa emlékeztetőül ezt a példát, hogy elkerülje a váratlan mutációkat okozó kódot.

11. Következtetés

Ebben az oktatóanyagban néhány bevált gyakorlatot és buktatót láthattunk a Java 8 lambda kifejezéseiben és funkcionális felületeiben. Az új funkciók hasznossága és ereje ellenére ezek csak eszközök. Minden fejlesztőnek figyelnie kell használatuk közben.

A teljes forráskód a példa ebben a GitHub projektben érhető el - ez egy Maven és Eclipse projekt, így importálható és használható is.