Hvorfor skal lokale variabler, der bruges i Lambdas, være endelige eller effektive endelige?

1. Introduktion

Java 8 giver os lambdas, og ved tilknytning, forestillingen om effektivt endelig variabler. Har du nogensinde spekuleret på, hvorfor lokale variabler fanget i lambdas skal være endelige eller effektivt endelige?

JLS giver os lidt af et tip, når det siger "Begrænsningen til effektivt endelige variabler forbyder adgang til dynamisk skiftende lokale variabler, hvis fangst sandsynligvis vil medføre samtidighedsproblemer." Men hvad betyder det?

I de næste sektioner vil vi grave dybere ned i denne begrænsning og se, hvorfor Java introducerede den. Vi viser eksempler til demonstration hvordan det påvirker enkeltgevindede og samtidige applikationer, og vi vil også afkæmpe et almindeligt antimønster for at omgå denne begrænsning.

2. Indfangning af Lambdas

Lambda-udtryk kan bruge variabler, der er defineret i et ydre omfang. Vi henviser til disse lambdas som fange lambdas. De kan fange statiske variabler, instansvariabler og lokale variabler, men kun lokale variabler skal være endelige eller effektivt endelige.

I tidligere Java-versioner løb vi ind i dette, da en anonym indre klasse fangede en variabel lokal til metoden, der omgav den - vi havde brug for at tilføje endelig nøgleord før den lokale variabel for at kompilatoren skal være glad.

Som lidt syntaktisk sukker kan compileren nu genkende situationer, hvor, mens endelig nøgleordet er ikke til stede, referencen ændrer sig slet ikke, hvilket betyder at det er effektivt endelig. Vi kan sige, at en variabel faktisk er endelig, hvis kompilatoren ikke klager, hvis vi erklærede den endelig.

3. Lokale variabler i fangst af Lambdas

Kort fortalt, dette kompilerer ikke:

Leverandørinkrementer (int start) {return () -> start ++; }

Start er en lokal variabel, og vi prøver at ændre den inde i et lambda-udtryk.

Den grundlæggende grund til, at dette ikke vil kompilere, er, at lambda er indfange værdien af Start, hvilket betyder at lave en kopi af det. At tvinge variablen til at være endelig undgår at give det indtryk, at den øges Start inde i lambda kunne faktisk ændre Start metode parameter.

Men hvorfor laver det en kopi? Læg mærke til, at vi returnerer lambda fra vores metode. Således løber lambda først efter Start metodeparameter samler skrald. Java skal lave en kopi af Start for at denne lambda skal leve uden for denne metode.

3.1. Problemer med samtidighed

For sjov, lad os forestille os et øjeblik, at Java gjorde tillade, at lokale variabler på en eller anden måde forbliver forbundet til deres fangede værdier.

Hvad skal vi gøre her:

public void localVariableMultithreading () {boolean run = true; executor.execute (() -> {while (run) {// gør operation}}); køre = falsk; }

Selvom dette ser uskyldigt ud, har det det snigende problem med "synlighed". Husk at hver tråd får sin egen stak, og så hvordan sikrer vi, at vores mens løkke ser ændringen til løb variabel i den anden stak? Svaret i andre sammenhænge kunne være at bruge synkroniseret blokke eller flygtige nøgleord.

Imidlertid, fordi Java pålægger den effektive endelige begrænsning, behøver vi ikke bekymre os om kompleksiteter som denne.

4. Statiske eller forekomstvariabler i fangst af lambdas

Eksemplerne før kan rejse nogle spørgsmål, hvis vi sammenligner dem med brugen af ​​statiske variabler eller instansvariabler i et lambda-udtryk.

Vi kan lave vores første eksempel ved at konvertere vores Start variabel til en instansvariabel:

privat int start = 0; Leverandørinkrementer () {return () -> start ++; }

Men hvorfor kan vi ændre værdien af Start her?

Kort sagt handler det om, hvor medlemsvariabler er gemt. Lokale variabler er på stakken, men medlemsvariabler er på bunken. Fordi vi har at gøre med bunkehukommelse, kan compileren garantere, at lambda har adgang til den nyeste værdi af Start.

Vi kan rette vores andet eksempel ved at gøre det samme:

privat flygtigt boolsk løb = sandt; public void instanceVariableMultithreading () {executor.execute (() -> {while (run) {// do operation}}); køre = falsk; }

Det løb variabel er nu synlig for lambda, selv når den udføres i en anden tråd, siden vi tilføjede flygtige nøgleord.

Generelt set kunne vi, når vi fanger en instansvariabel, tænke på den som at fange den endelige variabel det her. Alligevel, det faktum, at kompilatoren ikke klager, betyder ikke, at vi ikke skal tage forholdsregler, især i multithreading miljøer.

5. Undgå løsninger

For at omgå begrænsningen af ​​lokale variabler kan nogen tænke på at bruge variable indehavere til at ændre værdien af ​​en lokal variabel.

Lad os se et eksempel, der bruger en matrix til at gemme en variabel i en applikation med en enkelt gevind:

public int workaroundSingleThread () {int [] holder = new int [] {2}; IntStream sums = IntStream .of (1, 2, 3) .map (val -> val + holder [0]); holder [0] = 0; retur sums.sum (); }

Vi kunne tro, at strømmen summerer 2 til hver værdi, men det summerer faktisk 0, da dette er den seneste tilgængelige værdi, når lambda udføres.

Lad os gå et skridt videre og udføre summen i en anden tråd:

offentlig ugyldig workaroundMultithreading () {int [] holder = ny int [] {2}; Runnable runnable = () -> System.out.println (IntStream .of (1, 2, 3) .map (val -> val + holder [0]) .sum ()); ny tråd (kan køres) .start (); // simulering af nogle processer, prøv {Thread.sleep (new Random (). nextInt (3) * 1000L); } fange (InterruptedException e) {throw new RuntimeException (e); } holder [0] = 0; }

Hvilken værdi summerer vi her? Det afhænger af, hvor lang tid vores simulerede behandling tager. Hvis det er kort nok til at lade udførelsen af ​​metoden afsluttes, før den anden tråd udføres, udskrives den 6, ellers udskrives den 12.

Generelt er denne slags løsninger fejlbehæftede og kan give uforudsigelige resultater, så vi bør altid undgå dem.

6. Konklusion

I denne artikel har vi forklaret, hvorfor lambda-udtryk kun kan bruge endelige eller effektivt endelige lokale variabler. Som vi har set, kommer denne begrænsning fra forskellige variabler, og hvordan Java gemmer dem i hukommelsen. Vi har også vist farerne ved at bruge en fælles løsning.

Som altid er den fulde kildekode til eksemplerne tilgængelig på GitHub.