Waarom moeten lokale variabelen die in lambda's worden gebruikt definitief of effectief definitief zijn?

1. Inleiding

Java 8 geeft ons lambda's, en door associatie, het begrip effectief definitief variabelen. Heb je je ooit afgevraagd waarom lokale variabelen die in lambda's zijn vastgelegd definitief of effectief definitief moeten zijn?

Welnu, de JLS geeft ons een beetje een hint wanneer het zegt: "De beperking tot effectief definitieve variabelen verbiedt toegang tot dynamisch veranderende lokale variabelen, waarvan het vastleggen waarschijnlijk gelijktijdigheidsproblemen zou introduceren." Maar wat betekent het?

In de volgende secties zullen we dieper ingaan op deze beperking en zien waarom Java deze heeft geïntroduceerd. We laten voorbeelden zien om te demonstreren hoe het single-threaded en gelijktijdige applicaties beïnvloedt, en we zullen ook ontkracht een veelvoorkomend antipatroon om deze beperking te omzeilen.

2. Lambdas vangen

Lambda-expressies kunnen variabelen gebruiken die in een buitenste scope zijn gedefinieerd. We verwijzen naar deze lambda's als lambda's vastleggen. Ze kunnen statische variabelen, instantievariabelen en lokale variabelen vastleggen, maar alleen lokale variabelen moeten definitief of effectief definitief zijn.

In eerdere Java-versies kwamen we dit tegen toen een anonieme innerlijke klasse een variabele lokaal vastlegde voor de methode eromheen - we moesten de laatste trefwoord voor de lokale variabele zodat de compiler tevreden is.

Als een beetje syntactische suiker, kan de compiler nu situaties herkennen waarin, terwijl de laatste trefwoord is niet aanwezig, de referentie verandert helemaal niet, wat betekent dat het effectief laatste. We zouden kunnen zeggen dat een variabele in feite definitief is als de compiler niet zou klagen als we hem definitief zouden verklaren.

3. Lokale variabelen bij het vastleggen van Lambdas

Simpel gezegd, dit zal niet compileren:

Leverancier incrementer (int start) {return () -> start ++; }

begin is een lokale variabele en we proberen deze binnen een lambda-expressie te wijzigen.

De belangrijkste reden waarom dit niet zal compileren is dat de lambda dat wel is het vastleggen van de waarde van begin, wat betekent dat je er een kopie van maakt. Door de variabele definitief te maken, wordt voorkomen dat de indruk wordt gewekt dat deze stijgt begin binnenin de lambda zou de begin method parameter.

Maar waarom maakt het een kopie? Merk op dat we de lambda van onze methode retourneren. De lambda wordt dus pas uitgevoerd na de begin method parameter haalt garbage verzameld. Java moet een kopie maken van begin zodat deze lambda buiten deze methode kan leven.

3.1. Gelijktijdigheidsproblemen

Laten we ons even voorstellen dat Java deed laat lokale variabelen op de een of andere manier verbonden blijven met hun vastgelegde waarden.

Wat moeten we hier doen:

public void localVariableMultithreading () {boolean run = true; executor.execute (() -> {while (run) {// do operation}}); run = false; }

Hoewel dit er onschuldig uitziet, heeft het het verraderlijke probleem van "zichtbaarheid". Bedenk dat elke thread zijn eigen stapel krijgt, en hoe zorgen we ervoor dat onze terwijl lus ziet de wijziging van de rennen variabele in de andere stapel? Het antwoord in andere contexten zou kunnen zijn gesynchroniseerd blokken of de vluchtig trefwoord.

Echter, omdat Java de feitelijke laatste beperking oplegt, hoeven we ons geen zorgen te maken over dergelijke complexiteiten.

4. Statische of instantievariabelen bij het vastleggen van Lambdas

De bovenstaande voorbeelden kunnen enkele vragen oproepen als we ze vergelijken met het gebruik van statische of instantievariabelen in een lambda-uitdrukking.

We kunnen ons eerste voorbeeld compileren door onze begin variabele in een instantievariabele:

privé int start = 0; Leverancier incrementer () {return () -> start ++; }

Maar waarom kunnen we de waarde van begin hier?

Simpel gezegd, het gaat erom waar lidvariabelen worden opgeslagen. Lokale variabelen staan ​​op de stapel, maar lidvariabelen staan ​​op de stapel. Omdat we te maken hebben met heap-geheugen, kan de compiler garanderen dat de lambda toegang heeft tot de laatste waarde van begin.

We kunnen ons tweede voorbeeld repareren door hetzelfde te doen:

privé vluchtige booleaanse run = true; openbare ongeldige instanceVariableMultithreading () {executor.execute (() -> {while (run) {// operatie uitvoeren}}); run = false; }

De rennen variabele is nu zichtbaar voor de lambda, zelfs als deze in een andere thread wordt uitgevoerd sinds we de vluchtig trefwoord.

Over het algemeen kunnen we bij het vastleggen van een instantievariabele denken dat het de laatste variabele vastlegt dit. In ieder geval, het feit dat de compiler niet klaagt, betekent niet dat we geen voorzorgsmaatregelen moeten nemen, vooral in multithreading-omgevingen.

5. Voorkom tijdelijke oplossingen

Om de beperking op lokale variabelen te omzeilen, kan iemand overwegen om variabelhouders te gebruiken om de waarde van een lokale variabele te wijzigen.

Laten we eens kijken naar een voorbeeld dat een array gebruikt om een ​​variabele op te slaan in een single-threaded applicatie:

openbare int workaroundSingleThread () {int [] houder = nieuwe int [] {2}; IntStream sommen = IntStream .of (1, 2, 3) .map (val -> val + houder [0]); houder [0] = 0; return sums.sum (); }

We zouden kunnen denken dat de stream 2 optelt bij elke waarde, maar het is eigenlijk de som van 0 aangezien dit de laatste waarde is die beschikbaar is wanneer de lambda wordt uitgevoerd.

Laten we een stap verder gaan en de som in een andere thread uitvoeren:

openbare ongeldige oplossingMultithreading () {int [] houder = nieuwe int [] {2}; Runnable runnable = () -> System.out.println (IntStream .of (1, 2, 3) .map (val -> val + houder [0]) .sum ()); nieuwe thread (uitvoerbaar) .start (); // simuleer wat bewerkingen, probeer {Thread.sleep (new Random (). nextInt (3) * 1000L); } catch (InterruptedException e) {throw nieuwe RuntimeException (e); } houder [0] = 0; }

Welke waarde sommen we hier op? Het hangt af van hoe lang onze gesimuleerde verwerking duurt. Als het kort genoeg is om de uitvoering van de methode te laten eindigen voordat de andere thread wordt uitgevoerd, wordt er 6 afgedrukt, anders wordt er 12 afgedrukt.

Over het algemeen zijn dit soort tijdelijke oplossingen foutgevoelig en kunnen ze onvoorspelbare resultaten opleveren, dus we moeten ze altijd vermijden.

6. Conclusie

In dit artikel hebben we uitgelegd waarom lambda-expressies alleen definitieve of effectief laatste lokale variabelen kunnen gebruiken. Zoals we hebben gezien, komt deze beperking voort uit de verschillende aard van deze variabelen en hoe Java ze in het geheugen opslaat. We hebben ook de gevaren laten zien van het gebruik van een veelvoorkomende oplossing.

Zoals altijd is de volledige broncode voor de voorbeelden beschikbaar op GitHub.


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