Mengapa Pembolehubah Tempatan yang Digunakan dalam Lambdas Harus Muktamad atau Berkesan Akhir?

1. Pengenalan

Java 8 memberi kita lambdas, dan berdasarkan perkaitan, konsep pemboleh ubah akhir yang berkesan . Pernah terfikir mengapa pemboleh ubah tempatan yang ditangkap dalam lambdas mesti muktamad atau berkesan?

Nah, JLS memberi kita sedikit petunjuk ketika mengatakan "Pembatasan pemboleh ubah akhir secara efektif melarang akses ke pemboleh ubah lokal yang berubah secara dinamis, yang menangkapnya kemungkinan akan menimbulkan masalah bersamaan." Tetapi, apa maksudnya?

Pada bahagian seterusnya, kami akan menggali lebih mendalam mengenai batasan ini dan melihat mengapa Java memperkenalkannya. Kami akan menunjukkan contoh untuk menunjukkan bagaimana ia mempengaruhi aplikasi single-threaded dan serentak , dan kami juga akan membongkar anti-pola biasa untuk mengatasi sekatan ini.

2. Menangkap Lambdas

Ungkapan lambda dapat menggunakan pemboleh ubah yang ditentukan dalam skop luar. Kami menyebut lambda ini sebagai menangkap lambda . Mereka dapat menangkap pemboleh ubah statik, pemboleh ubah contoh, dan pemboleh ubah tempatan, tetapi hanya pemboleh ubah tempatan yang mesti muktamad atau yang efektif.

Dalam versi Java sebelumnya, kami mengalami ini apabila kelas dalaman tanpa nama menangkap pemboleh ubah tempatan dengan kaedah yang mengelilinginya - kami perlu menambahkan kata kunci terakhir sebelum pemboleh ubah tempatan agar penyusun gembira.

Sebagai sedikit gula sintaktik, kini pengkompil boleh mengenali situasi di mana, manakala akhir kata kunci tidak hadir, rujukan tidak berubah sama sekali, bermakna ia berkesan akhir. Kita boleh mengatakan bahawa pemboleh ubah adalah muktamad jika pengkompil tidak mengeluh sekiranya kita menyatakannya sebagai muktamad.

3. Pemboleh ubah Tempatan dalam Menangkap Lambdas

Ringkasnya, ini tidak akan menyusun:

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

start adalah pemboleh ubah tempatan, dan kami berusaha mengubahnya di dalam ungkapan lambda.

Sebab asas ini tidak dapat disusun adalah kerana lambda menangkap nilai permulaan , yang bermaksud membuat salinannya. Memaksa pemboleh ubah menjadi akhir menghindari memberi kesan bahawa kenaikan awal di dalam lambda sebenarnya dapat mengubah parameter kaedah mulai .

Tetapi, mengapa ia membuat salinan? Baiklah, perhatikan bahawa kami mengembalikan lambda dari kaedah kami. Oleh itu, lambda tidak akan dapat dijalankan sehingga setelah parameter kaedah mula mengumpulkan sampah. Java harus membuat salinan permulaan agar lambda ini hidup di luar kaedah ini.

3.1. Isu Serentak

Untuk keseronokan, mari kita bayangkan untuk seketika bahawa Jawa tidak membenarkan pembolehubah tempatan entah bagaimana kekal disambungkan kepada nilai ditangkap mereka.

Apa yang harus kita lakukan di sini:

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

Walaupun ini terlihat tidak bersalah, ia mempunyai masalah "visibilitas" yang berbahaya. Ingat bahawa setiap utas mendapat timbunannya sendiri, dan bagaimana kita memastikan bahawa gelung sementara kita melihat perubahan pada pemboleh ubah run di timbunan yang lain? Jawapannya dalam konteks lain boleh menggunakan blok yang disegerakkan atau kata kunci yang tidak stabil .

Namun, kerana Java mengenakan sekatan akhir yang efektif, kita tidak perlu bimbang tentang kerumitan seperti ini.

4. Pembolehubah Statik atau Instance dalam Menangkap Lambdas

Contoh sebelum ini dapat menimbulkan beberapa persoalan jika kita membandingkannya dengan penggunaan pemboleh ubah statik atau contoh dalam ungkapan lambda.

Kita dapat menyusun contoh pertama kita hanya dengan menukar pemboleh ubah permulaan kita menjadi pemboleh ubah contoh:

private int start = 0; Supplier incrementer() { return () -> start++; }

Tetapi, mengapa kita boleh mengubah nilai permulaan di sini?

Ringkasnya, ini mengenai tempat pemboleh ubah anggota disimpan. Pemboleh ubah tempatan berada di timbunan, tetapi pemboleh ubah anggota berada di timbunan. Kerana kita berurusan dengan memori timbunan, penyusun dapat menjamin bahawa lambda akan dapat mengakses nilai permulaan terbaru .

Kami dapat memperbaiki contoh kedua kami dengan melakukan perkara yang sama:

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

The jangka berubah-ubah kini boleh dilihat dengan lambda yang walaupun ia dilaksanakan di thread lain kerana kita menambah menentu kata kunci.

Secara umum, ketika menangkap pemboleh ubah contoh, kita dapat menganggapnya sebagai menangkap pemboleh ubah terakhir ini . Bagaimanapun, fakta bahawa penyusun tidak mengeluh tidak bermaksud bahawa kita tidak harus mengambil langkah berjaga-jaga, terutama di persekitaran multithreading.

5. Elakkan Penyelesaian Masalah

Untuk mengatasi sekatan pemboleh ubah tempatan, seseorang mungkin berfikir menggunakan pemegang pemboleh ubah untuk mengubah nilai pemboleh ubah tempatan.

Mari lihat contoh yang menggunakan tatasusunan untuk menyimpan pemboleh ubah dalam aplikasi utas tunggal:

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(); }

Kami dapat berfikir bahawa aliran menjumlahkan 2 untuk setiap nilai, tetapi sebenarnya menjumlahkan 0 kerana ini adalah nilai terbaru yang tersedia ketika lambda dijalankan.

Mari melangkah lebih jauh dan laksanakan jumlahnya dalam urutan lain:

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; }

Nilai apa yang kita simpulkan di sini? Ia bergantung pada berapa lama pemprosesan simulasi kami mengambil masa. Sekiranya cukup pendek untuk membiarkan pelaksanaan kaedah berakhir sebelum utas lain dijalankan, ia akan mencetak 6, jika tidak, ia akan mencetak 12.

Secara amnya, jalan keluar seperti ini mudah dijangkiti ralat dan boleh menghasilkan hasil yang tidak dapat diramalkan, jadi kita harus selalu menghindarinya.

6. Kesimpulannya

Dalam artikel ini, kami telah menjelaskan mengapa ungkapan lambda hanya dapat menggunakan pemboleh ubah tempatan akhir atau akhir. Seperti yang telah kita lihat, batasan ini berasal dari sifat pemboleh ubah yang berbeza dan bagaimana Java menyimpannya dalam memori. Kami juga telah menunjukkan bahaya menggunakan penyelesaian biasa.

Seperti biasa, kod sumber penuh untuk contoh boleh didapati di GitHub.