Java >> Java tutorial >  >> Tag >> final

Hvorfor skal lokale variabler, der bruges i lambdaer, være endelige eller faktisk endelige?

1. Introduktion

Java 8 giver os lambdaer, og ved tilknytning begrebet effektivt endeligt variabler. Har du nogensinde undret dig over, hvorfor lokale variabler, der fanges i lambdas, skal være endelige eller faktisk endelige?

Nå, JLS giver os et lille hint, når det siger "Begrænsningen til effektivt endelige variabler forbyder adgang til dynamisk skiftende lokale variabler, hvis indfangning sandsynligvis vil introducere samtidighedsproblemer." Men hvad betyder det?

I de næste afsnit vil vi grave dybere ned i denne begrænsning og se, hvorfor Java introducerede den. Vi viser eksempler for at demonstrere hvordan det påvirker enkelttrådede og samtidige applikationer , og vi vil også aflæse et almindeligt anti-mønster til at omgå denne begrænsning.

2. Indfangning af lambdaer

Lambda-udtryk kan bruge variabler defineret i et ydre omfang. Vi omtaler disse lambdaer som fangende lambdaer . De kan fange statiske variabler, instansvariabler og lokale variabler, men kun lokale variabler skal være endelige eller faktisk endelige.

I tidligere Java-versioner stødte vi på dette, da en anonym indre klasse fangede en variabel lokal til den metode, der omgav den – vi var nødt til at tilføje den endelige nøgleord før den lokale variabel for at compileren skal være tilfreds.

Som lidt syntaktisk sukker kan compileren nu genkende situationer, hvor den endelige  søgeord er ikke til stede, referencen ændrer sig slet ikke, hvilket betyder, at det er effektivt endelig. Vi kunne sige, at en variabel faktisk er endelig, hvis compileren ikke ville klage, hvis vi erklærer den endelig.

3. Lokale variabler i indfangning af lambdaer

Kort sagt, dette kompilerer ikke:

Supplier<Integer> incrementer(int start) {
  return () -> start++;
}

start  er en lokal variabel, og vi forsøger at ændre den inde i et lambda-udtryk.

Den grundlæggende årsag til, at dette ikke kompileres, er, at lambda'en fanger værdien af ​​start , hvilket betyder at lave en kopi af det. Ved at tvinge variablen til at være endelig undgår du at give indtryk af, at stigende start inde i lambdaen kunne faktisk ændre starten metode parameter.

Men hvorfor laver den en kopi? Nå, læg mærke til, at vi returnerer lambdaen fra vores metode. Således vil lambdaen ikke køre før efter starten metodeparameter får affald indsamlet. Java skal lave en kopi af start for at denne lambda kan leve uden for denne metode.

3.1. Samtidighedsproblemer

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

Hvad skal vi gøre her:

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

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

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

4. Statiske eller instansvariabler i indfangning af lambdaer

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

Vi kan få vores første eksempel til at kompilere blot ved at konvertere vores start variabel til en instansvariabel:

private int start = 0;

Supplier<Integer> incrementer() {
    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å heapen. Fordi vi har at gøre med heap-hukommelse, kan compileren garantere, at lambdaen vil have adgang til den seneste værdi af start.

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

private volatile boolean run = true;

public void instanceVariableMultithreading() {
    executor.execute(() -> {
        while (run) {
            // do operation
        }
    });

    run = false;
}

kør  variabel er nu synlig for lambda, selv når den udføres i en anden tråd, da vi tilføjede den flygtige  søgeord.

Generelt set, når vi fanger en forekomstvariabel, kunne vi tænke på det som at fange den endelige variabel dette . Anyway, det faktum, at compileren ikke klager, betyder ikke, at vi ikke skal tage forholdsregler, især i multithreading-miljøer.

5. Undgå løsninger

For at komme uden om begrænsningen af ​​lokale variabler, kan nogen tænke på at bruge variabelholdere til at ændre værdien af ​​en lokal variabel.

Lad os se et eksempel, der bruger et array til at gemme en variabel i en enkelt-trådet applikation:

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

    holder[0] = 0;

    return sums.sum();
}

Vi kunne tro, at streamen summerer 2 til hver værdi, men den summerer faktisk 0, da dette er den seneste værdi, der er tilgængelig, når lambdaen udføres.

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

public void workaroundMultithreading() {
    int[] holder = new int[] { 2 };
    Runnable runnable = () -> System.out.println(IntStream
      .of(1, 2, 3)
      .map(val -> val + holder[0])
      .sum());

    new Thread(runnable).start();

    // simulating some processing
    try {
        Thread.sleep(new Random().nextInt(3) * 1000L);
    } catch (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 den er kort nok til at lade udførelsen af ​​metoden afslutte, før den anden tråd udføres, udskriver den 6, ellers udskriver den 12.

Generelt er disse former for løsninger tilbøjelige til at fejle 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 faktisk endelige lokale variabler. Som vi har set, kommer denne begrænsning fra disse variables forskellige natur, 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.


Java tag