Lambda-udtryk og funktionelle grænseflader: tip og bedste praksis
1. Oversigt
Nu hvor Java 8 har nået bred anvendelse, er mønstre og bedste praksis begyndt at dukke op for nogle af dets overskriftsfunktioner. I denne vejledning vil vi se nærmere på funktionelle grænseflader og lambda-udtryk.
2. Foretrækker standard funktionelle grænseflader
Funktionelle grænseflader, som er samlet i java.util.function pakke, tilfredsstille de fleste udvikleres behov for at levere måltyper til lambda-udtryk og metodehenvisninger. Hver af disse grænseflader er generelle og abstrakte, hvilket gør dem lette at tilpasse sig næsten ethvert lambda-udtryk. Udviklere bør udforske denne pakke, før de opretter nye funktionelle grænseflader.
Overvej en grænseflade Foo:
@FunctionalInterface offentlig grænseflade Foo {Strengmetode (strengstreng); }
og en metode tilføje() i nogle klasser UseFoo, som tager denne grænseflade som en parameter:
public String add (String string, Foo foo) {return foo.method (string); }
For at udfø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 intet andet end en funktion, der accepterer et argument og producerer et resultat. Java 8 leverer allerede en sådan grænseflade i Fungere fra java.util.function-pakken.
Nu kan vi fjerne grænsefladen Foo helt og skift vores kode til:
public String add (String string, Function fn) {return fn.apply (string); }
For at udføre dette kan vi skrive:
Funktion fn = parameter -> parameter + "fra lambda"; String result = useFoo.add ("Message", fn);
3. Brug @FunktionelInterface Kommentar
Kommenter dine funktionelle grænseflader med @FunktionelInterface. Først synes denne kommentar at være ubrugelig. Selv uden det bliver din grænseflade behandlet som funktionel, så længe den kun har en abstrakt metode.
Men forestil dig et stort projekt med flere grænseflader - det er svært at kontrollere alt manuelt. En grænseflade, der var designet til at være funktionel, kunne ved et uheld ændres ved at tilføje andre abstrakte metoder / metoder, hvilket gør den ubrugelig som en funktionel grænseflade.
Men ved hjælp af @FunktionelInterface kommentar, vil compileren udløse en fejl som svar på ethvert forsøg på at bryde den foruddefinerede struktur af en funktionel grænseflade. Det er også et meget praktisk værktøj til at gøre din applikationsarkitektur lettere at forstå for andre udviklere.
Så brug dette:
@FunctionalInterface offentlig grænseflade Foo {Strengmetode (); }
i stedet for bare:
offentlig grænseflade Foo {Strengmetode (); }
4. Overbrug ikke standardmetoder i funktionelle grænseflader
Vi kan nemt tilføje standardmetoder til den funktionelle grænseflade. Dette er acceptabelt for den funktionelle interface-kontrakt, så længe der kun er en abstrakt metodedeklaration:
@FunctionalInterface offentlig grænseflade Foo {Strengmetode (strengstreng); standard ugyldigt defaultMethod () {}}
Funktionelle grænseflader kan udvides med andre funktionelle grænseflader, hvis deres abstrakte metoder har den samme signatur.
For eksempel:
@FunctionalInterface public interface FooExtended udvider Baz, Bar {} @FunctionalInterface public interface Baz {Strengmetode (strengstreng); standard String defaultBaz () {}} @FunctionalInterface offentlig grænseflade Bar {Strengmetode (Strengstreng); standard String defaultBar () {}}
Ligesom med almindelige grænseflader, at udvide forskellige funktionelle grænseflader med den samme standardmetode kan være problematisk.
Lad os f.eks. Tilføje standardCommon () metode til Bar og Baz grænseflader:
@FunctionalInterface offentlig grænseflade Baz {Strengmetode (strengstreng); standard String defaultBaz () {} standard String defaultCommon () {}} @FunctionalInterface public interface Bar {Strengmetode (Strengstreng); standard String defaultBar () {} standard String defaultCommon () {}}
I dette tilfælde får vi en kompileringsfejl:
interface FooExtended arver ikke-relaterede standardindstillinger for defaultCommon () fra typerne Baz og Bar ...
For at løse dette, standardCommon () metoden skal tilsidesættes i FooExtended interface. Vi kan selvfølgelig tilbyde en tilpasset implementering af denne metode. Imidlertid, Vi kan også genbruge implementeringen fra den overordnede grænseflade:
@FunctionalInterface offentlig grænseflade FooExtended udvider Baz, Bar {@Override standard String defaultCommon () {return Bar.super.defaultCommon (); }}
Men vi skal være forsigtige. Tilføjelse af for mange standardmetoder til grænsefladen er ikke en særlig god arkitektonisk beslutning. Dette bør betragtes som et kompromis, der kun skal bruges, når det er nødvendigt, til opgradering af eksisterende grænseflader uden at bryde bagudkompatibilitet.
5. Instantier funktionelle grænseflader med Lambda-udtryk
Compileren giver dig mulighed for at bruge en indre klasse til at starte en funktionel grænseflade. Dette kan dog føre til meget detaljeret kode. Du foretrækker lambda-udtryk:
Foo foo = parameter -> parameter + "fra Foo";
over en indre klasse:
Foo fooByIC = ny Foo () {@Override offentlig strengmetode (strengstreng) {returstreng + "fra Foo"; }};
Lambda-ekspressionsmetoden kan bruges til enhver egnet grænseflade fra gamle biblioteker. Det kan bruges til grænseflader som f.eks Kan køres, Komparator, og så videre. Men dette betyder ikke, at du skal gennemse hele din ældre codebase og ændre alt.
6. Undgå overbelastningsmetoder med funktionelle grænseflader som parametre
Brug metoder med forskellige navne for at undgå kollisioner; lad os se på et eksempel:
offentlig grænseflade Processor {String proces (kaldbar c) kaster undtagelse; Strengeproces (leverandør); } offentlig klasse ProcessorImpl implementerer processor {@Override offentlig strengproces (kaldbar c) kaster undtagelse {// implementeringsoplysninger} @Override offentlig strengproces (leverandør) {// implementeringsoplysninger}}
Ved første øjekast virker dette rimeligt. Men ethvert forsøg på at udføre en af ProcessorImpl'S metoder:
Strengresultat = processor.process (() -> "abc");
ender med en fejl med følgende meddelelse:
henvisning til proces er tvetydig både metodeproces (java.util.concurrent.Callable) i com.baeldung.java8.lambda.tips.ProcessorImpl og method process (java.util.function.Supplier) i com.baeldung.java8.lambda. tip.ProcessorImpl match
For at løse dette problem har vi to muligheder. Den første er at bruge metoder med forskellige navne:
String processWithCallable (Callable c) kaster Undtagelse; StrengprocesWithSupplier (leverandør (er))
Det andet er at udføre casting manuelt. Dette foretrækkes ikke.
Strengresultat = processor.process ((leverandør) () -> "abc");
7. Behandl ikke Lambda-udtryk som indre klasser
På trods af vores tidligere eksempel, hvor vi i det væsentlige erstattede indre klasse med et lambda-udtryk, er de to begreber forskellige på en vigtig måde: omfang.
Når du bruger en indre klasse, skaber den et nyt omfang. Du kan skjule lokale variabler fra det vedlagte område ved at starte nye lokale variabler med de samme navne. Du kan også bruge nøgleordet det her inde i din indre klasse som en henvisning til dens forekomst.
Lambda-udtryk fungerer dog med et lukket omfang. Du kan ikke skjule variabler fra det lukkede omfang inde i lambdas krop. I dette tilfælde nøgleordet det her er en henvisning til en lukkende instans.
For eksempel i klassen UseFoo du har en instansvariabel værdi:
privat strengværdi = "Omslutningsomfangsværdi";
Derefter placerer du følgende kode i en eller anden metode i denne klasse og udfører denne metode.
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 denne. værdi; }; String resultLambda = fooLambda.method (""); returner "Resultater: resultatIC =" + resultatIC + ", resultatLambda =" + resultatLambda; }
Hvis du udfører scopeExperiment () metode, får du følgende resultat: Resultater: resultatIC = Indre klasseværdi, resultLambda = Omslutningsomfangsværdi
Som du kan se, ved at ringe dette. værdi i IC kan du få adgang til en lokal variabel fra dens forekomst. Men i tilfælde af lambda, dette. værdi opkald giver dig adgang til variablen værdi som er defineret i UseFoo klasse, men ikke til variablen værdi defineret inde i lambdas krop.
8. Hold Lambda-udtryk korte og forklarer sig ikke selv
Brug om muligt en linjekonstruktion i stedet for en stor blok kode. Husk lambdas skal være enudtryk, ikke en fortælling. På trods af sin koncise syntaks, lambdas skal præcist udtrykke den funktionalitet, de leverer.
Dette er hovedsagelig stilistisk rådgivning, da ydeevne ikke vil ændre sig drastisk. Generelt er det dog meget lettere at forstå og arbejde med en sådan kode.
Dette kan opnås på mange måder - lad os se nærmere på det.
8.1. Undgå kodeblokke i Lambdas krop
I en ideel situation skal lambdas skrives i en kodelinje. Med denne tilgang er lambda en selvforklarende konstruktion, der erklærer, hvilken handling der skal udføres med hvilke data (i tilfælde af lambdas med parametre).
Hvis du har en stor blok kode, er lambdas funktionalitet ikke umiddelbart klar.
Med dette i tankerne skal du gøre følgende:
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 = "Something" + parameter; // mange linjer med kode returnerer resultat; };
Brug dog ikke denne ”en-line lambda” -regel som dogme. Hvis du har to eller tre linjer i lambdas definition, er det muligvis ikke værdifuldt at udtrække koden til en anden metode.
8.2. Undgå at specificere parametertyper
En kompilator er i de fleste tilfælde i stand til at løse typen af lambda-parametre ved hjælp af skriv slutning. Derfor er det valgfrit at tilføje en type til parametrene og kan udelades.
Gør dette:
(a, b) -> a.toLowerCase () + b.toLowerCase ();
i stedet for dette:
(String a, String b) -> a.toLowerCase () + b.toLowerCase ();
8.3. Undgå parenteser omkring en enkelt parameter
Lambda-syntaks kræver kun parenteser omkring mere end en parameter, eller når der slet ikke er nogen parameter. Derfor er det sikkert at gøre din kode lidt kortere og at udelukke parenteser, når der kun er én parameter.
Så gør dette:
a -> a.toLowerCase ();
i stedet for dette:
(a) -> a.toLowerCase ();
8.4. Undgå returnerklæring og seler
Seler og Vend tilbage udsagn er valgfri i en-line lambda-kroppe. Dette betyder, at de kan udelades for klarhed og kortfattethed.
Gør dette:
a -> a.toLowerCase ();
i stedet for dette:
a -> {returner a.toLowerCase ()};
8.5. Brug referencer til metode
Meget ofte, selv i vores tidligere eksempler, kalder lambda-udtryk bare metoder, som allerede er implementeret andre steder. I denne situation er det meget nyttigt at bruge en anden Java 8-funktion: metodehenvisninger.
Så lambda-udtrykket:
a -> a.toLowerCase ();
kunne erstattes af:
String :: toLowerCase;
Dette er ikke altid kortere, men det gør koden mere læselig.
9. Brug “Effektivt endelige” variabler
Adgang til en ikke-endelig variabel inde i lambda-udtryk vil forårsage kompileringstidsfejl. Men det betyder ikke, at du skal markere hver målvariabel som endelig.
Ifølge "effektivt endelig”Koncept, en kompilator behandler hver variabel som endelig, så længe det kun er tildelt en gang.
Det er sikkert at bruge sådanne variabler inde i lambdas, fordi compileren vil kontrollere deres tilstand og udløse en kompileringstidsfejl umiddelbart efter ethvert forsøg på at ændre dem.
For eksempel kompilerer følgende kode ikke:
public void method () {String localVariable = "Local"; Foo foo = parameter -> {String localVariable = parameter; returner localVariable; }; }
Compileren vil informere dig om, at:
Variablen 'localVariable' er allerede defineret i omfanget.
Denne tilgang skal forenkle processen med at gøre lambda-eksekvering trådsikker.
10. Beskyt objektvariabler mod mutation
Et af hovedformålene med lambdas er brug i parallel computing - hvilket betyder, at de er virkelig nyttige, når det kommer til trådsikkerhed.
Det “effektivt endelige” paradigme hjælper meget her, men ikke i alle tilfælde. Lambdas kan ikke ændre en værdi af et objekt fra at omfatte omfang. Men i tilfælde af mutable objektvariabler kunne en tilstand ændres inden for lambda-udtryk.
Overvej følgende kode:
int [] total = ny int [1]; Kan køres r = () -> i alt [0] ++; r.run ();
Denne kode er lovlig, som Total variabel forbliver "effektivt endelig". Men vil det objekt, det refererer til, have den samme tilstand efter udførelsen af lambda? Ingen!
Hold dette eksempel som en påmindelse om at undgå kode, der kan forårsage uventede mutationer.
11. Konklusion
I denne vejledning så vi nogle bedste praksis og faldgruber i Java 8s lambda-udtryk og funktionelle grænseflader. På trods af nytten og kraften i disse nye funktioner er de bare værktøjer. Hver udvikler skal være opmærksom, mens han bruger dem.
Det komplette kildekode eksemplet er tilgængeligt i dette GitHub-projekt - dette er et Maven- og Eclipse-projekt, så det kan importeres og bruges som det er.