Miért kell a lambdákban használt helyi változóknak véglegesnek vagy ténylegesen véglegesnek lenniük?

1. Bemutatkozás

A Java 8 megadja nekünk a lambdákat, és az asszociáció révén a gyakorlatilag végleges változók. Elgondolkodott már azon, hogy miért kell a lambdákban rögzített helyi változóknak véglegeseknek vagy ténylegesen véglegeseknek lenniük?

Nos, a JLS ad nekünk egy kis tippet, amikor azt mondja: „A tényleges végső változók korlátozása tiltja a dinamikusan változó helyi változókhoz való hozzáférést, amelyek rögzítése valószínűleg egyidejűségi problémákat vet fel.” De mit jelent ez?

A következő szakaszokban mélyebben belemerülünk ebbe a korlátozásba, és megtudjuk, miért vezette be a Java. Mutatunk példákat a bemutatásra hogyan befolyásolja az egyszálú és egyidejű alkalmazásokat, és mi is megtesszük elvet egy közös anti-mintát a korlátozás kiküszöbölése érdekében.

2. Lambdas elfogása

A Lambda kifejezések külső hatókörben definiált változókat használhatnak. Ezekre a lambdákra hivatkozunk lambdas elfogása. Statikus változókat, példányváltozókat és helyi változókat képesek rögzíteni, de csak a helyi változóknak véglegesnek vagy ténylegesen véglegesnek kell lenniük.

Korábbi Java verziókban akkor találkoztunk ezzel, amikor egy névtelen belső osztály egy változó helyi értéket rögzített az azt körülvevő módszerhez - hozzá kellett adnunk a végső kulcsszó a helyi változó előtt, hogy a fordító boldog legyen.

Kicsit szintaktikus cukorként most a fordító felismeri azokat a helyzeteket, ahol, míg a végső kulcsszó nincs, a hivatkozás egyáltalán nem változik, vagyis az hatékonyan végső. Mondhatnánk, hogy egy változó ténylegesen végleges, ha a fordító nem panaszkodna, ha véglegesnek nyilvánítanánk.

3. Helyi változók a lambdas elfogásában

Egyszerűen fogalmazva, ez nem állítja össze:

Beszállítói növekmény (int start) {return () -> start ++; }

Rajt egy lokális változó, és megpróbáljuk módosítani egy lambda kifejezés belsejében.

Ennek az az oka, hogy ez nem áll össze, az az, hogy a lambda az értékének megragadása Rajt, vagyis másolat készítése. A változó véglegesre kényszerítése elkerüli a növekmény benyomását Rajt a lambda belsejében valóban módosíthatja a Rajt method paraméter.

De miért készít másolatot? Nos, vegye észre, hogy a lambdát visszaküldjük a módszerünkből. Így a lambda csak a Rajt A method paraméter összegyűjti a szemetet. A Java-nak másolatot kell készítenie Rajt annak érdekében, hogy ez a lambda ezen a módszeren kívül éljen.

3.1. Egyidejűségi kérdések

A móka kedvéért képzeljük el egy pillanatra azt a Java-t tette lehetővé teszi a helyi változók számára, hogy valahogy kapcsolatban maradjanak elfogott értékeikkel.

Mit tegyünk itt:

public void localVariableMultithreading () {logikai futás = igaz; végrehajtó.execute (() -> {while (futtatás) {// művelet végrehajtása}}); fut = hamis; }

Ez ugyan ártatlannak tűnik, de alattomos problémája van a „láthatóság”. Emlékezzünk vissza arra, hogy minden szálnak megvan a maga vereme, és így tudjuk biztosítani, hogy a míg hurok lát változás a fuss változó a másik veremben? A válasz más összefüggésekben a következő lehet: szinkronizált blokkok vagy a illó kulcsszó.

Azonban, mivel a Java ténylegesen végleges korlátozást szab ki, nem kell aggódnunk az ilyen bonyolultságok miatt.

4. Statikus vagy példányváltozók a lambdák rögzítésében

Az előző példák felvethetnek néhány kérdést, ha összehasonlítjuk őket statikus vagy példányváltozók lambda-kifejezésben való használatával.

Első példánkat csak a mi átalakításával készíthetjük Rajt változó egy példány változóba:

privát int start = 0; Beszállítói növekmény () {return () -> start ++; }

De miért változtathatjuk meg az értékét Rajt itt?

Egyszerűen fogalmazva, arról van szó, hogy a tagváltozókat hol tárolják. A helyi változók a veremben vannak, de a tagváltozók a kupacban vannak. Mivel halom memóriával van dolgunk, a fordító garantálhatja, hogy a lambda hozzáférjen a legújabb értékéhez Rajt.

A második példánkat ugyanezzel kijavíthatjuk:

privát volatile boolean run = true; public void instanceVariableMultithreading () {végrehajtó.execute (() -> {while (futtatás) {// művelet}}); fut = hamis; }

A fuss változó már akkor is látható a lambda számára, ha egy másik szálban futtatják, mióta hozzáadtuk a illó kulcsszó.

Általánosságban elmondható, hogy egy példányváltozó rögzítésekor úgy gondolhatunk rá, mint a végső változó elfogására ez. Egyébként is, az a tény, hogy a fordító nem panaszkodik, nem jelenti azt, hogy ne tegyünk óvintézkedéseket, főleg többszálas környezetben.

5. Kerülje a megoldásokat

Annak érdekében, hogy megkerülje a helyi változók korlátozását, valaki gondolkodhat azon, hogy változótulajdonosokkal módosítja a helyi változó értékét.

Lássunk egy példát, amely tömböt használ egy változó tárolására egyszálú alkalmazásban:

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

Azt gondolhatnánk, hogy a folyam 2-et összesít az egyes értékekhez, de valójában 0-t összegez, mivel ez a legfrissebb elérhető érték a lambda végrehajtásakor.

Menjünk egy lépéssel tovább, és hajtsuk végre az összeget egy másik szálban:

public void workaroundMultithreading () {int [] holder = new int [] {2}; Futható futható = () -> System.out.println (IntStream .of (1, 2, 3) .map (val -> val + tartó [0]) .sum ()); új Szál (futható) .start (); // néhány feldolgozás szimulálása: próbáld ki a {Thread.sleep (new Random (). nextInt (3) * 1000L); } catch (InterruptedException e) {dobjon új RuntimeException (e); } tartó [0] = 0; }

Milyen értéket összegezünk itt? Attól függ, hogy a szimulált feldolgozásunk mennyi ideig tart. Ha elég rövid ahhoz, hogy a metódus végrehajtása befejeződjön, mielőtt a másik szál végrehajtásra kerülne, akkor 6-ot nyomtat, különben 12-et.

Általában az ilyen megoldások hibára hajlamosak és kiszámíthatatlan eredményeket hozhatnak, ezért ezeket mindig kerülnünk kell.

6. Következtetés

Ebben a cikkben elmagyaráztuk, hogy a lambda kifejezések miért használhatják csak a végső vagy hatékonyan a végső helyi változókat. Mint láttuk, ez a korlátozás e változók eltérő jellegéből és abból adódik, hogy a Java hogyan tárolja őket a memóriában. Megmutattuk a közös megoldás használatának veszélyeit is.

Mint mindig, a példák teljes forráskódja elérhető a GitHubon.