Lambda-uttrykk og funksjonelle grensesnitt: tips og beste praksis

1. Oversikt

Nå som Java 8 har nådd bred bruk, har mønstre og beste praksis begynt å dukke opp for noen av hovedtrekkfunksjonene. I denne opplæringen vil vi se nærmere på funksjonelle grensesnitt og lambdauttrykk.

2. Foretrekker standard funksjonelle grensesnitt

Funksjonelle grensesnitt, som er samlet i java.util.funksjon pakke, tilfredsstille de fleste utvikleres behov når det gjelder å tilby måltyper for lambdauttrykk og metodereferanser. Hver av disse grensesnittene er generelle og abstrakte, noe som gjør dem enkle å tilpasse seg nesten ethvert lambdauttrykk. Utviklere bør utforske denne pakken før de oppretter nye funksjonelle grensesnitt.

Vurder et grensesnitt Foo:

@FunctionalInterface offentlig grensesnitt Foo {Strengmetode (strengstreng); }

og en metode legge til() i noen klasse UseFoo, som tar dette grensesnittet som en parameter:

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

For å utføre det, ville du skrive:

Foo foo = parameter -> parameter + "fra lambda"; String result = useFoo.add ("Message", foo);

Se nærmere, så ser du det Foo er ikke noe mer enn en funksjon som godtar ett argument og produserer et resultat. Java 8 gir allerede et slikt grensesnitt i Funksjon fra java.util.function-pakken.

Nå kan vi fjerne grensesnittet Foo fullstendig og endre koden vår til:

public String add (String string, Function fn) {return fn.apply (string); }

For å utføre dette kan vi skrive:

Funksjon fn = parameter -> parameter + "fra lambda"; Strengresultat = useFoo.add ("Melding", fn);

3. Bruk @FunctionalInterface Kommentar

Kommenter dine funksjonelle grensesnitt med @FunctionalInterface. Først ser denne kommentaren ut til å være ubrukelig. Selv uten det blir grensesnittet ditt behandlet som funksjonelt så lenge det bare har en abstrakt metode.

Men forestill deg et stort prosjekt med flere grensesnitt - det er vanskelig å kontrollere alt manuelt. Et grensesnitt, som ble designet for å være funksjonelt, kunne ved et uhell endres ved å legge til andre abstrakte metoder / metoder, noe som gjør det ubrukelig som et funksjonelt grensesnitt.

Men ved hjelp av @FunctionalInterface merknad, vil kompilatoren utløse en feil som svar på ethvert forsøk på å bryte den forhåndsdefinerte strukturen til et funksjonelt grensesnitt. Det er også et veldig praktisk verktøy for å gjøre applikasjonsarkitekturen lettere å forstå for andre utviklere.

Så bruk dette:

@FunctionalInterface offentlig grensesnitt Foo {Strengmetode (); }

i stedet for bare:

offentlig grensesnitt Foo {Strengmetode (); }

4. Ikke bruk for mye standardmetoder i funksjonelle grensesnitt

Vi kan enkelt legge til standardmetoder i det funksjonelle grensesnittet. Dette er akseptabelt for den funksjonelle grensesnittkontrakten så lenge det bare er en abstrakt metodedeklarasjon:

@FunctionalInterface offentlig grensesnitt Foo {Strengmetode (strengstreng); standard ugyldig defaultMethod () {}}

Funksjonelle grensesnitt kan utvides med andre funksjonelle grensesnitt hvis deres abstrakte metoder har samme signatur.

For eksempel:

@FunctionalInterface public interface FooExtended utvider Baz, Bar {} @FunctionalInterface public interface Baz {Strengmetode (strengstreng); standard String defaultBaz () {}} @FunctionalInterface public interface Bar {Strengmetode (strengstreng); standard streng defaultBar () {}}

Akkurat som med vanlige grensesnitt, å utvide forskjellige funksjonelle grensesnitt med samme standardmetode kan være problematisk.

La oss for eksempel legge til defaultCommon () metoden til Bar og Baz grensesnitt:

@FunctionalInterface offentlig grensesnitt Baz {Strengmetode (strengstreng); default String defaultBaz () {} default String defaultCommon () {}} @FunctionalInterface public interface Bar {Strengmetode (strengstreng); standard streng defaultBar () {} standard streng standardCommon () {}}

I dette tilfellet får vi en kompileringstidsfeil:

grensesnitt FooExtended arver ikke-relaterte standardinnstillinger for defaultCommon () fra typene Baz og Bar ...

For å fikse dette, defaultCommon () metoden skal overstyres i FooExtended grensesnitt. Vi kan selvfølgelig tilby en tilpasset implementering av denne metoden. Derimot, vi kan også gjenbruke implementeringen fra foreldregrensesnittet:

@FunctionalInterface offentlig grensesnitt FooExtended utvider Baz, Bar {@Override standard String defaultCommon () {return Bar.super.defaultCommon (); }}

Men vi må være forsiktige. Å legge for mange standardmetoder til grensesnittet er ikke en veldig god arkitektonisk beslutning. Dette bør betraktes som et kompromiss, bare for å brukes når det er nødvendig, for å oppgradere eksisterende grensesnitt uten å bryte bakoverkompatibilitet.

5. Instantier funksjonelle grensesnitt med Lambda-uttrykk

Kompilatoren lar deg bruke en indre klasse til å sette i gang et funksjonelt grensesnitt. Dette kan imidlertid føre til veldig utførlig kode. Du bør foretrekke lambdauttrykk:

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

over en indre klasse:

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

Lambda-uttrykksmetoden kan brukes til ethvert passende grensesnitt fra gamle biblioteker. Det er brukbart for grensesnitt som Kjørbar, Komparator, og så videre. Imidlertid dette betyr ikke at du bør gjennomgå hele din eldre kodebase og endre alt.

6. Unngå overbelastningsmetoder med funksjonelle grensesnitt som parametere

Bruk metoder med forskjellige navn for å unngå kollisjoner; la oss se på et eksempel:

offentlig grensesnitt Prosessor {Strengprosess (Callable c) kaster Unntak; Strengprosess (leverandør); } offentlig klasse ProcessorImpl implementerer prosessor {@Override offentlig strengprosess (kallbar c) kaster unntak {// implementeringsdetaljer} @Override offentlig strengprosess (leverandør) {// implementeringsdetaljer}}

Ved første øyekast virker dette rimelig. Men ethvert forsøk på å utføre noen av ProsessorImplSine metoder:

Strengresultat = prosessor.prosess (() -> "abc");

ender med en feil med følgende melding:

referanse til prosess er tvetydig både metodeprosessen (java.util.concurrent.Callable) i com.baeldung.java8.lambda.tips.ProcessorImpl og method process (java.util.function.Supplier) i com.baeldung.java8.lambda. tips.ProsessorImpl-kamp

For å løse dette problemet har vi to alternativer. Den første er å bruke metoder med forskjellige navn:

StrengprosessWithCallable (Callable c) kaster Unntak; StrengprosessWithSupplier (Leverandør (er));

Det andre er å utføre casting manuelt. Dette er ikke foretrukket.

Strengresultat = prosessor.prosess ((leverandør) () -> "abc");

7. Ikke behandle Lambda-uttrykk som indre klasser

Til tross for vårt forrige eksempel, hvor vi i det vesentlige erstattet indre klasse med et lambdauttrykk, er de to begrepene forskjellige på en viktig måte: omfang.

Når du bruker en indre klasse, skaper den et nytt omfang. Du kan skjule lokale variabler fra det vedlagte omfanget ved å sette i gang nye lokale variabler med samme navn. Du kan også bruke nøkkelordet dette inne i din indre klasse som en referanse til sin forekomst.

Imidlertid fungerer lambdauttrykk med omsluttende omfang. Du kan ikke skjule variabler fra det omsluttende omfanget inne i lambdas kropp. I dette tilfellet nøkkelordet dette er en referanse til en omsluttende forekomst.

For eksempel i klassen UseFoo du har en forekomstvariabel verdi:

private strengverdi = "vedlegger omfangsverdi";

Deretter plasserer du følgende kode i noen av metodene i denne klassen og utfører denne metoden.

public String scopeExperiment () {Foo fooIC = new Foo () {String value = "Inner class value"; @ Override public String method (String string) {return this.value; }}; StrengresultatIC = fooIC.method (""); Foo fooLambda = parameter -> {String value = "Lambda value"; returner dette. verdi; }; String resultLambda = fooLambda.method (""); returner "Resultater: resultatIC =" + resultatIC + ", resultatLambda =" + resultatLambda; }

Hvis du utfører scopeExperiment () metode, får du følgende resultat: Resultater: resultatIC = Verdi for indre klasse, resultLambda = Omfatter omfangsverdi

Som du kan se, ved å ringe denne verdien i IC kan du få tilgang til en lokal variabel fra forekomsten. Men når det gjelder lambda, denne verdien samtale gir deg tilgang til variabelen verdi som er definert i UseFoo klasse, men ikke til variabelen verdi definert inne i lambdas kropp.

8. Hold Lambda Expressions Short og Selvforklarende

Hvis mulig, bruk en linjekonstruksjon i stedet for en stor blokk med kode. Huske lambdas skal være enuttrykk, ikke en fortelling. Til tross for den konsise syntaksen, lambdas bør presist uttrykke funksjonaliteten de gir.

Dette er hovedsakelig stilråd, da ytelse ikke vil endre seg drastisk. Generelt er det imidlertid mye lettere å forstå og å jobbe med en slik kode.

Dette kan oppnås på mange måter - la oss se nærmere på det.

8.1. Unngå blokker av kode i Lambdas kropp

I en ideell situasjon bør lambdas skrives i en kodelinje. Med denne tilnærmingen er lambda en selvforklarende konstruksjon, som erklærer hvilken handling som skal utføres med hvilke data (i tilfelle lambdas med parametere).

Hvis du har en stor blokk med kode, er ikke lambdas funksjonalitet umiddelbart klar.

Gjør følgende med tanke på dette:

Foo foo = parameter -> buildString (parameter);
private String buildString (String parameter) {String result = "Something" + parameter; // mange linjer med kode returnerer resultat; }

i stedet for:

Foo foo = parameter -> {String result = "Noe" + parameter; // mange linjer med kode returnerer resultat; };

Du må imidlertid ikke bruke denne "en-linjens lambda" -regelen som dogme. Hvis du har to eller tre linjer i lambdas definisjon, er det kanskje ikke verdifullt å trekke ut koden til en annen metode.

8.2. Unngå å spesifisere parametertyper

En kompilator er i de fleste tilfeller i stand til å løse typen lambda-parametere ved hjelp av skriv slutning. Derfor er det valgfritt å legge til en type i parameterne og kan utelates.

Gjør dette:

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

istedenfor dette:

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

8.3. Unngå parenteser rundt en enkelt parameter

Lambda-syntaks krever parentes bare rundt mer enn én parameter, eller når det ikke er noen parameter i det hele tatt. Derfor er det trygt å gjøre koden litt kortere og å ekskludere parenteser når det bare er én parameter.

Så gjør dette:

a -> a.toLowerCase ();

istedenfor dette:

(a) -> a.toLowerCase ();

8.4. Unngå returerklæring og seler

Seler og komme tilbake uttalelser er valgfrie i en-linjers lambda-kropper. Dette betyr at de kan utelates for klarhet og konsistens.

Gjør dette:

a -> a.toLowerCase ();

istedenfor dette:

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

8.5. Bruk referanser

Svært ofte, selv i våre tidligere eksempler, kaller lambda-uttrykk bare metoder som allerede er implementert andre steder. I denne situasjonen er det veldig nyttig å bruke en annen Java 8-funksjon: metodereferanser.

Så lambdauttrykket:

a -> a.toLowerCase ();

kan erstattes av:

String :: toLowerCase;

Dette er ikke alltid kortere, men det gjør koden mer lesbar.

9. Bruk "Effektivt endelige" variabler

Å få tilgang til en ikke-endelig variabel i lambda-uttrykk vil føre til kompileringstidsfeilen. Men det betyr ikke at du skal merke hver målvariabel som endelig.

Ifølge "effektivt endelig”Konsept, behandler en kompilator hver variabel som endelig, så lenge det bare er tildelt en gang.

Det er trygt å bruke slike variabler inne i lambdas fordi kompilatoren vil kontrollere deres tilstand og utløse en kompileringstidsfeil umiddelbart etter ethvert forsøk på å endre dem.

Følgende kode vil for eksempel ikke kompilere:

public void method () {String localVariable = "Local"; Foo foo = parameter -> {String localVariable = parameter; returner lokalVariabel; }; }

Kompilatoren vil informere deg om at:

Variabel 'localVariable' er allerede definert i omfanget.

Denne tilnærmingen skal forenkle prosessen med å gjøre lambda-kjøring trådsikker.

10. Beskytt objektvariabler mot mutasjon

Et av hovedformålene med lambdas er bruk i parallell databehandling - noe som betyr at de er veldig nyttige når det gjelder trådsikkerhet.

Det "effektivt endelige" paradigmet hjelper mye her, men ikke i alle tilfeller. Lambdas kan ikke endre en verdi av et objekt fra å omslutte omfang. Men når det gjelder foranderlige objektvariabler, kan en tilstand endres i lambdauttrykk.

Vurder følgende kode:

int [] total = ny int [1]; Kjørbar r = () -> totalt [0] ++; r.run ();

Denne koden er lovlig, som Total variabel forblir "effektivt endelig". Men vil objektet det refereres til ha samme tilstand etter henrettelsen av lambda? Nei!

Hold dette eksemplet som en påminnelse om å unngå kode som kan forårsake uventede mutasjoner.

11. Konklusjon

I denne opplæringen så vi noen gode fremgangsmåter og fallgruver i Java 8s lambdauttrykk og funksjonelle grensesnitt. Til tross for nytten og kraften til disse nye funksjonene, er de bare verktøy. Hver utvikler bør ta hensyn når du bruker dem.

Det komplette kildekode for eksemplet er tilgjengelig i dette GitHub-prosjektet - dette er et Maven og Eclipse-prosjekt, slik at det kan importeres og brukes som det er.


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