Zašto lokalne varijable koje se koriste u lambdama moraju biti konačne ili efektivno konačne?

1. Uvod

Java 8 daje nam lambde, a udruživanjem i pojam efektivno konačan varijable. Jeste li se ikad zapitali zašto lokalne varijable zabilježene u lambdama moraju biti konačne ili zapravo konačne?

Pa, JLS nam daje mali nagovještaj kada kaže: "Ograničenje na efektivne konačne varijable zabranjuje pristup dinamički promjenjivim lokalnim varijablama, čije bi hvatanje vjerojatno uvelo probleme s paralelnošću." Ali, što to znači?

U sljedećim ćemo odjeljcima dublje istražiti ovo ograničenje i vidjeti zašto ga je Java uvela. Pokazat ćemo primjere za demonstraciju kako utječe na jednonitne i istodobne primjene, a mi ćemo također razotkriti uobičajeni obrazac za zaobilaženje ovog ograničenja.

2. Hvatanje Lambda

Lambda izrazi mogu koristiti varijable definirane u vanjskom opsegu. Te lambde nazivamo: hvatanje lambda. Mogu hvatati statičke varijable, varijable instance i lokalne varijable, ali samo lokalne varijable moraju biti konačne ili zapravo konačne.

U ranijim Java verzijama naletjeli smo na to kad je anonimna unutarnja klasa uhvatila varijablu local u metodu koja ju je okruživala - trebali smo dodati konačni ključna riječ prije lokalne varijable kako bi prevodilac bio sretan.

Kao malo sintaktičkog šećera, sada sastavljač može prepoznati situacije u kojima, dok konačni ključna riječ nije prisutna, referenca se uopće ne mijenja, što znači da jest učinkovito konačni. Mogli bismo reći da je varijabla zapravo konačna ako se prevodilac ne bi žalio da smo je proglasili konačnom.

3. Lokalne varijable u hvatanju lambda

Jednostavno rečeno, ovo se neće kompajlirati:

Inkrementer dobavljača (int start) {return () -> start ++; }

početak je lokalna varijabla i pokušavamo je izmijeniti unutar lambda izraza.

Osnovni razlog zbog kojeg se ovo neće kompajlirati je taj što je lambda hvatanje vrijednosti početak, što znači napraviti njegovu kopiju. Prisiljavanjem varijable da bude konačna izbjegava se stvoriti dojam da se povećava početak unutar lambde mogao bi zapravo izmijeniti početak parametar metode.

Ali, zašto pravi kopiju? Pa, primijetite da vraćamo lambdu iz naše metode. Dakle, lambda će se pokrenuti tek nakon početak parametar metode dobiva prikupljeno smeće. Java mora napraviti kopiju početak kako bi ova lambda živjela izvan ove metode.

3.1. Pitanja o istodobnosti

Iz zabave, zamislimo na trenutak tu Javu učinio dopustiti lokalnim varijablama da nekako ostanu povezane sa svojim zarobljenim vrijednostima.

Što da radimo ovdje:

javna praznina localVariableMultithreading () {boolean run = true; executor.execute (() -> {while (run) {// do операcija}}); trčanje = lažno; }

Iako ovo izgleda nevino, ima podmukli problem "vidljivosti". Sjetimo se da svaka nit dobiva svoj vlastiti stog, pa kako onda osigurati da naš dok petlja vidi promjena u trčanje varijabla u drugom stogu? Odgovor u drugim kontekstima mogao bi biti upotreba sinkronizirano blokovi ili hlapljiv ključna riječ.

Međutim, jer Java nameće efektivno konačno ograničenje, ne moramo se brinuti zbog složenosti poput ove.

4. Statičke ili instance varijable u hvatanju lambda

Primjeri prije mogu postaviti neka pitanja ako ih usporedimo s upotrebom statičkih ili varijabli instance u lambda izrazu.

Prvi primjer možemo kompajlirati pretvaranjem našeg početak varijabla u varijablu instance:

privatni int početak = 0; Inkrementer dobavljača () {return () -> start ++; }

Ali, zašto možemo promijeniti vrijednost početak ovdje?

Jednostavno rečeno, radi se o tome gdje su pohranjene varijable člana. Lokalne varijable nalaze se na stogu, ali varijable članova nalaze se na hrpi. Budući da imamo posla s memorijom hrpe, kompajler može jamčiti da će lambda imati pristup najnovijoj vrijednosti početak.

Svoj drugi primjer možemo popraviti radeći isto:

privatno volatilno logičko pokretanje = true; public void instanceVariableMultithreading () {executor.execute (() -> {while (run) {// do операcija}}); trčanje = lažno; }

The trčanje varijabla je sada vidljiva lambda čak i kad se izvršava u drugoj niti otkad smo dodali hlapljiv ključna riječ.

Općenito govoreći, kad hvatamo varijablu instance, mogli bismo to smatrati hvatanjem konačne varijable ovaj. U svakom slučaju, činjenica da se sastavljač ne žali ne znači da ne bismo trebali poduzimati mjere predostrožnosti, posebno u okruženjima s više niti.

5. Izbjegavajte zaobilaznice

Da bi se zaobišlo ograničenje lokalnih varijabli, netko bi mogao smisliti korištenje držača varijabli za modificiranje vrijednosti lokalne varijable.

Pogledajmo primjer koji koristi niz za pohranu varijable u jednonitnu aplikaciju:

public int workaroundSingleThread () {int [] držač = novi int [] {2}; IntStream sume = IntStream .of (1, 2, 3) .map (val -> val + držač [0]); držač [0] = 0; vratiti sume.sum (); }

Mogli bismo misliti da tok zbraja 2 za svaku vrijednost, ali zapravo se zbraja 0, jer je ovo najnovija vrijednost dostupna kada se izvrši lambda.

Idemo korak dalje i izvršimo zbroj u drugoj niti:

zaobilaženje javne praznineMultithreading () {int [] držač = novi int [] {2}; Izvodljivo izvodljivo = () -> System.out.println (IntStream .of (1, 2, 3) .map (val -> val + držač [0]) .sum ()); nova nit (pokrenuta) .start (); // simuliranje neke obrade pokušaj {Thread.sleep (novi Random (). nextInt (3) * 1000L); } catch (InterruptedException e) {throw new RuntimeException (e); } držač [0] = 0; }

Koju vrijednost ovdje sumiramo? Ovisi o tome koliko traje naša simulirana obrada. Ako je dovoljno kratak da dozvoli da se izvršavanje metode završi prije nego što se izvrši druga nit, ispisat će 6, u suprotnom, ispisat će 12.

Općenito, ove vrste zaobilaženja podložne su pogreškama i mogu donijeti nepredvidive rezultate, pa ih uvijek treba izbjegavati.

6. Zaključak

U ovom smo članku objasnili zašto lambda izrazi mogu koristiti samo konačne ili efektivno konačne lokalne varijable. Kao što smo vidjeli, ovo ograničenje dolazi iz različite prirode ovih varijabli i načina na koji ih Java pohranjuje u memoriju. Pokazali smo i opasnosti korištenja uobičajenog zaobilaznog rješenja.

Kao i uvijek, puni izvorni kod za primjere dostupan je na GitHubu.