Výrazy lambda a funkčné rozhrania: tipy a osvedčené postupy

1. Prehľad

Teraz, keď Java 8 dosiahla široké využitie, začali sa objavovať osvedčené postupy pre niektoré z jej hlavných funkcií. V tomto tutoriáli sa bližšie pozrieme na funkčné rozhrania a výrazy lambda.

2. Uprednostňujte štandardné funkčné rozhrania

Funkčné rozhrania, ktoré sú zhromaždené v java.util.funkcia balík, uspokojuje potreby väčšiny vývojárov pri poskytovaní cieľových typov pre výrazy lambda a odkazy na metódy. Každé z týchto rozhraní je všeobecné a abstraktné, vďaka čomu sa dajú ľahko prispôsobiť takmer každému výrazu lambda. Vývojári by mali tento balíček preskúmať pred vytvorením nových funkčných rozhraní.

Zvážte rozhranie Foo:

@FunctionalInterface verejné rozhranie Foo {Metóda reťazca (reťazec reťazca); }

a metóda pridať () v nejakej triede UseFoo, ktorá berie toto rozhranie ako parameter:

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

Ak to chcete vykonať, napíšete:

Foo foo = parameter -> parameter + "from lambda"; Výsledok reťazca = useFoo.add ("Správa", foo);

Pozri sa bližšie a uvidíš to Foo nie je nič iné ako funkcia, ktorá prijíma jeden argument a vytvára výsledok. Java 8 už takéto rozhranie poskytuje v Funkcia z balíka java.util.function.

Teraz môžeme odstrániť rozhranie Foo úplne a zmeňte náš kód na:

public String add (reťazec reťazca, funkcia fn) {návrat fn.apply (reťazec); }

Aby sme to mohli vykonať, môžeme napísať:

Funkcia fn = parameter -> parameter + "from lambda"; Výsledok reťazca = useFoo.add ("Správa", fn);

3. Použite @Funkčné rozhranie Anotácia

Anotujte svoje funkčné rozhrania @Funkčné rozhranie. Spočiatku sa táto anotácia javí ako zbytočná. Aj bez neho bude vaše rozhranie považované za funkčné, ak má iba jednu abstraktnú metódu.

Predstavte si však veľký projekt s niekoľkými rozhraniami - je ťažké ovládať všetko ručne. Rozhranie, ktoré bolo navrhnuté tak, aby bolo funkčné, sa mohlo omylom zmeniť pridaním inej abstraktnej metódy / metód, čím sa stalo nepoužiteľným ako funkčné rozhranie.

Ale pomocou @Funkčné rozhranie anotáciu, kompilátor spustí chybu v reakcii na akýkoľvek pokus o prelomenie preddefinovanej štruktúry funkčného rozhrania. Je to tiež veľmi užitočný nástroj na uľahčenie pochopenia architektúry vašej aplikácie pre ostatných vývojárov.

Použite teda toto:

@FunctionalInterface verejné rozhranie Foo {String method (); }

namiesto iba:

verejné rozhranie Foo {String method (); }

4. Nepoužívajte nadmerné použitie predvolených metód vo funkčných rozhraniach

Do funkčného rozhrania môžeme ľahko pridať predvolené metódy. To je prijateľné pre zmluvu o funkčnom rozhraní, pokiaľ existuje iba jedna abstraktná deklarácia metódy:

@FunctionalInterface verejné rozhranie Foo {Metóda reťazca (reťazec reťazca); default void defaultMethod () {}}

Funkčné rozhrania je možné rozšíriť o ďalšie funkčné rozhrania, ak majú ich abstraktné metódy rovnaký podpis.

Napríklad:

@FunctionalInterface verejné rozhranie FooExtended rozširuje Baz, Bar {} @FunctionalInterface verejné rozhranie Baz {metóda reťazca (reťazec reťazca); default String defaultBaz () {}} @FunctionalInterface panel verejného rozhrania {metóda reťazca (reťazec reťazca); predvolený reťazec defaultBar () {}}

Rovnako ako u bežných rozhraní, rozšírenie rôznych funkčných rozhraní rovnakou predvolenou metódou môže byť problematické.

Napríklad pridajme defaultCommon () metóda do Bar a Baz rozhrania:

@FunctionalInterface verejné rozhranie Baz {metóda reťazca (reťazec reťazca); default String defaultBaz () {} default String defaultCommon () {}} @FunctionalInterface Bar verejného rozhrania {String method (String string); default String defaultBar () {} default String defaultCommon () {}}

V tomto prípade sa zobrazí chyba pri kompilácii:

rozhranie FooExtended dedí nesúvisiace predvolené hodnoty pre defaultCommon () z typov Baz a Bar ...

Ak to chcete opraviť, defaultCommon () metóda by mala byť prepísaná v FooExtended rozhranie. Túto metódu môžeme samozrejme zabezpečiť na mieru. Avšak implementáciu môžeme opätovne použiť aj z nadradeného rozhrania:

@FunctionalInterface verejné rozhranie FooExtended rozširuje Baz, Bar {@Override default String defaultCommon () {return Bar.super.defaultCommon (); }}

Musíme však byť opatrní. Pridať príliš veľa predvolených metód do rozhrania nie je veľmi dobré architektonické rozhodnutie. Toto by sa malo považovať za kompromis, ktorý sa má použiť, iba ak je to potrebné, pri aktualizácii existujúcich rozhraní bez narušenia spätnej kompatibility.

5. Vytvorte funkčné rozhrania pomocou výrazov lambda

Kompilátor vám umožní použiť vnútornú triedu na vytvorenie inštancie funkčného rozhrania. To však môže viesť k veľmi podrobnému kódu. Mali by ste uprednostniť výrazy lambda:

Foo foo = parameter -> parameter + "from Foo";

nad vnútornou triedou:

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

Prístup lambda expresie je možné použiť pre akékoľvek vhodné rozhranie zo starých knižníc. Je použiteľný pre rozhrania ako Spustiteľné, Komparátor, a tak ďalej. Avšak toto to neznamená, že by ste mali skontrolovať celú svoju staršiu databázu kódov a všetko zmeniť.

6. Vyvarujte sa preťažovaniu metód s funkčnými rozhraniami ako parametrami

Aby ste predišli kolíziám, použite metódy s rôznymi názvami; pozrime sa na príklad:

verejné rozhranie Procesor {Proces reťazca (Vyvolávateľný c) vyvolá Výnimku; Proces reťazca (dodávatelia); } public class ProcessorImpl implementuje Processor {@Override public String process (Callable c) throws Exception {// implementation details} @Override public String process (Supplier s) {// implementation details}}

Na prvý pohľad sa to zdá byť rozumné. Ale akýkoľvek pokus o vykonanie niektorého z ProcessorImplMetódy:

Výsledok reťazca = processor.process (() -> "abc");

končí chybou s nasledujúcou správou:

odkaz na proces je nejednoznačný tak proces metódy (java.util.concurrent.Callable) v com.baeldung.java8.lambda.tips.ProcessorImpl, ako aj proces procesu (java.util.function.Supplier) v com.baeldung.java8.lambda. tipy. Zhoda ProcessorImpl

Na vyriešenie tohto problému máme dve možnosti. Prvým je použitie metód s rôznymi názvami:

String processWithCallable (Callable c) vyvolá výnimku; String processWithSupplier (Dodávatelia);

Druhým je ručné liatie. Toto nie je preferované.

Výsledok reťazca = processor.process ((Supplier) () -> "abc");

7. Nezaobchádzajte s prejavmi lambda ako s vnútornými triedami

Napriek nášmu predchádzajúcemu príkladu, kde sme v podstate nahradili vnútornú triedu výrazom lambda, sa tieto dva pojmy dôležitým spôsobom líšia: rozsah.

Keď použijete vnútornú triedu, vytvorí sa nový rozsah. Lokálne premenné môžete skryť z ohraničujúceho rozsahu vytvorením inštancie nových lokálnych premenných s rovnakými názvami. Môžete tiež použiť kľúčové slovo toto vo vašej vnútornej triede ako odkaz na jej inštanciu.

Výrazy lambda však fungujú s ohraničujúcim rozsahom. Nemôžete skryť premenné z ohraničujúceho rozsahu v tele lambdy. V tomto prípade kľúčové slovo toto je odkaz na priloženú inštanciu.

Napríklad v triede UseFoo máte premennú inštancie hodnota:

private String value = "Ohradzujúca hodnota rozsahu";

Potom do niektorej metódy tejto triedy vložte nasledujúci kód a vykonajte túto metódu.

public String scopeExperiment () {Foo fooIC = new Foo () {String value = "Hodnota vnútornej triedy"; @Override public String method (String string) {return this.value; }}; Výsledok reťazcaIC = fooIC.method (""); Foo fooLambda = parameter -> {String value = "Lambda value"; vrátiť túto.hodnotu; }; Reťazec resultLambda = fooLambda.method (""); návrat "Výsledky: resultIC =" + resultIC + ", resultLambda =" + resultLambda; }

Ak vykonáte scopeExperiment () metódou, získate nasledujúci výsledok: Výsledky: resultIC = hodnota vnútornej triedy, resultLambda = obklopujúca hodnota rozsahu

Ako vidíte, volaním táto.hodnota v IC máte prístup k miestnej premennej z jej inštancie. Ale v prípade lambdy táto.hodnota volanie vám umožní prístup k premennej hodnotu ktorý je definovaný v UseFoo triedy, ale nie do premennej hodnotu definované vo vnútri lambda tela.

8. Výrazy lambda udržujte krátke a zrozumiteľné

Pokiaľ je to možné, použite namiesto veľkého bloku kódu jednoriadkové konštrukcie. Pamätaj lambdas by mal byťvýraz, nie naratív. Napriek svojej stručnej syntaxi lambdas by mali presne vyjadrovať funkčnosť, ktorú poskytujú.

Ide hlavne o štylistické rady, pretože výkon sa drasticky nezmení. Všeobecne je však s týmto kódom oveľa jednoduchšie porozumieť a pracovať s ním.

To sa dá dosiahnuť mnohými spôsobmi - pozrime sa bližšie.

8.1. Vyvarujte sa blokovaniu kódu v tele Lambdy

V ideálnom prípade by mali byť lambdy napísané v jednom riadku kódu. Pri tomto prístupe je lambda samozrejmou konštrukciou, ktorá deklaruje, aká akcia by sa mala vykonať s akými údajmi (v prípade lambdas s parametrami).

Ak máte veľký blok kódu, funkčnosť lambdy nie je okamžite jasná.

V tejto súvislosti postupujte takto:

Foo foo = parameter -> buildString (parameter);
private String buildString (String parameter) {String result = "Something" + parameter; // veľa riadkov výsledku vrátenia kódu; }

namiesto:

Foo foo = parameter -> {Výsledok reťazca = "Niečo" + parameter; // veľa riadkov výsledku vrátenia kódu; };

Nepoužívajte však toto pravidlo „jedného riadku lambda“ ako dogmu. Ak máte v definícii lambdy dva alebo tri riadky, nemusí byť užitočné tento kód extrahovať do inej metódy.

8.2. Vyhýbajte sa zadávaniu typov parametrov

Kompilátor je vo väčšine prípadov schopný vyriešiť typ parametrov lambda pomocou odvodenie typu. Preto je pridanie typu k parametrom voliteľné a možno ho vynechať.

To urobiť:

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

namiesto toho:

(Reťazec a, Reťazec b) -> a.toLowerCase () + b.toLowerCase ();

8.3. Nepoužívajte zátvorky okolo jedného parametra

Syntax lambda vyžaduje zátvorky iba okolo viac ako jedného parametra alebo ak neexistuje žiadny parameter. Preto je bezpečné váš kód trochu skrátiť a vylúčiť zátvorky, ak existuje iba jeden parameter.

Urobte toto:

a -> a.toLowerCase ();

namiesto toho:

(a) -> a.toLowerCase ();

8.4. Nepoužívajte vyhlásenie o vrátení a zložené zátvorky

Rovnátka a návrat vyhlásenia sú v jednoriadkových telách lambda voliteľné. To znamená, že je možné ich kvôli jasnosti a stručnosti vynechať.

To urobiť:

a -> a.toLowerCase ();

namiesto toho:

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

8.5. Použite odkaz na metódu

Aj v našich predchádzajúcich príkladoch veľmi často výrazy lambda iba volajú metódy, ktoré sú už inde implementované. V tejto situácii je veľmi užitočné použiť inú funkciu Java 8: referencie metód.

Takže výraz lambda:

a -> a.toLowerCase ();

možno nahradiť:

String :: toLowerCase;

Nie je to vždy kratšie, ale zvyšuje to čitateľnosť kódu.

9. Používajte premenné „Efektívne konečné“

Prístup k nekončnej premennej vo vnútri výrazov lambda spôsobí chybu v čase kompilácie. Neznamená to však, že by ste mali označiť každú cieľovú premennú ako konečné.

Podľa "efektívne konečné”, Kompilátor zaobchádza s každou premennou ako s konečné, pokiaľ je pridelený iba raz.

Je bezpečné používať takéto premenné vo vnútri lambdas, pretože kompilátor bude riadiť ich stav a spustí chybu kompilácie ihneď po každom pokuse o ich zmenu.

Nasledujúci kód sa napríklad nebude kompilovať:

public void method () {String localVariable = "Local"; Foo foo = parameter -> {String localVariable = parameter; návrat localVariable; }; }

Kompilátor vás bude informovať, že:

Premenná 'localVariable' je už v rozsahu definovaná.

Tento prístup by mal zjednodušiť proces zaistenia bezpečnosti vykonávania lambda s vláknami.

10. Chráňte premenné objektov pred mutáciou

Jedným z hlavných účelov lambdas je použitie v paralelnom výpočte - čo znamená, že sú skutočne užitočné, pokiaľ ide o bezpečnosť vlákien.

Paradigma „efektívne záverečná“ tu veľmi pomáha, ale nie vo všetkých prípadoch. Lambdas nemôže zmeniť hodnotu objektu z ohraničujúceho rozsahu. Ale v prípade premenných premenlivých objektov sa mohol stav zmeniť vo vnútri výrazov lambda.

Zvážte nasledujúci kód:

int [] total = new int [1]; Spustiteľné r = () -> celkom [0] ++; r.run ();

Tento zákonník je legálny, nakoľko Celkom premenná zostáva „efektívne konečná“. Ale bude mať objekt, na ktorý odkazuje, rovnaký stav po vykonaní lambdy? Nie!

Tento príklad si ponechajte na pripomenutie, aby ste sa vyhli kódu, ktorý môže spôsobiť neočakávané mutácie.

11. Záver

V tomto tutoriáli sme videli niektoré osvedčené postupy a úskalia vo výrazoch lambda a funkčných rozhraniach Java 8. Napriek užitočnosti a sile týchto nových funkcií sú to iba nástroje. Každý vývojár by mal pri ich používaní venovať pozornosť.

Kompletný zdrojový kód príklad je k dispozícii v tomto projekte GitHub - jedná sa o projekt Maven a Eclipse, takže ho možno importovať a použiť tak, ako je.


$config[zx-auto] not found$config[zx-overlay] not found