Mengapa Variabel Lokal yang Digunakan di Lambdas Harus Final atau Secara Efektif Final?

1. Perkenalan

Java 8 memberi kita lambda, dan berdasarkan asosiasi, gagasan tentang variabel akhir yang efektif . Pernah bertanya-tanya mengapa variabel lokal yang ditangkap dalam lambda harus final atau final secara efektif?

Nah, JLS memberi kita sedikit petunjuk ketika dikatakan "Pembatasan untuk variabel akhir yang efektif melarang akses ke variabel lokal yang berubah secara dinamis, yang penangkapannya kemungkinan akan menimbulkan masalah konkurensi." Tapi apa maksudnya?

Di bagian selanjutnya, kita akan menggali lebih dalam tentang batasan ini dan melihat mengapa Java memperkenalkannya. Kami akan menunjukkan contoh untuk mendemonstrasikan bagaimana hal itu mempengaruhi aplikasi single-threaded dan bersamaan , dan kami juga akan menghilangkan prasangka anti-pola umum untuk mengatasi pembatasan ini.

2. Menangkap Lambdas

Ekspresi lambda dapat menggunakan variabel yang ditentukan dalam lingkup luar. Kami menyebut lambda ini sebagai lambda menangkap . Mereka dapat menangkap variabel statis, variabel instan, dan variabel lokal, tetapi hanya variabel lokal yang harus final atau final secara efektif.

Di versi Java sebelumnya, kami mengalami ini ketika kelas dalam anonim menangkap variabel lokal ke metode yang mengelilinginya - kami perlu menambahkan kata kunci terakhir sebelum variabel lokal agar kompiler senang.

Sebagai sedikit gula sintaksis, sekarang compiler dapat mengenali situasi di mana, sementara kata kunci terakhir tidak ada, referensinya tidak berubah sama sekali, artinya itu efektif secara final. Kita dapat mengatakan bahwa variabel secara efektif final jika kompilator tidak akan mengeluh jika kita menyatakannya sebagai final.

3. Variabel Lokal dalam Menangkap Lambdas

Sederhananya, ini tidak akan bisa dikompilasi:

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

start adalah variabel lokal, dan kami mencoba memodifikasinya di dalam ekspresi lambda.

Alasan dasar ini tidak dapat dikompilasi adalah karena lambda menangkap nilai awal , yang berarti membuat salinannya. Memaksa variabel menjadi final menghindari kesan bahwa incrementing start di dalam lambda sebenarnya dapat mengubah parameter metode start .

Tapi, mengapa itu membuat salinan? Nah, perhatikan bahwa kami mengembalikan lambda dari metode kami. Jadi, lambda tidak akan dijalankan sampai parameter metode start mendapatkan sampah yang dikumpulkan. Java harus membuat salinan awal agar lambda ini berada di luar metode ini.

3.1. Masalah Konkurensi

Untuk bersenang-senang, mari kita bayangkan sejenak bahwa Jawa tidak memungkinkan variabel lokal entah bagaimana tetap terhubung ke nilai-nilai mereka ditangkap.

Apa yang harus kita lakukan di sini:

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

Meskipun ini terlihat tidak bersalah, ini memiliki masalah "visibilitas" yang berbahaya. Ingatlah bahwa setiap utas mendapatkan tumpukannya sendiri, jadi bagaimana kita memastikan bahwa loop sementara kita melihat perubahan ke variabel run di tumpukan lain? Jawaban dalam konteks lain dapat menggunakan blok tersinkronisasi atau kata kunci yang mudah menguap .

Namun, karena Java memberlakukan batasan akhir yang efektif, kami tidak perlu khawatir tentang kerumitan seperti ini.

4. Variabel Statis atau Instans dalam Menangkap Lambdas

Contoh-contoh sebelumnya dapat menimbulkan beberapa pertanyaan jika kita membandingkannya dengan penggunaan variabel statis atau instance dalam ekspresi lambda.

Kita dapat membuat contoh kompilasi pertama kita hanya dengan mengubah variabel awal kita menjadi variabel contoh:

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

Tapi, mengapa kita bisa mengubah nilai start disini?

Sederhananya, ini tentang di mana variabel anggota disimpan. Variabel lokal ada di tumpukan, tetapi variabel anggota ada di tumpukan. Karena kita berurusan dengan memori heap, kompilator dapat menjamin bahwa lambda akan memiliki akses ke nilai start terbaru .

Kita dapat memperbaiki contoh kedua kita dengan melakukan hal yang sama:

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

The run variabel sekarang terlihat lambda bahkan ketika itu dijalankan di thread lain karena kita menambahkan volatil kata kunci.

Secara umum, ketika menangkap variabel instan, kita bisa menganggapnya sebagai menangkap variabel terakhir ini . Bagaimanapun, fakta bahwa kompilator tidak mengeluh tidak berarti bahwa kita tidak boleh mengambil tindakan pencegahan, terutama di lingkungan multithreading.

5. Hindari Solusi

Untuk menghindari batasan pada variabel lokal, seseorang mungkin berpikir untuk menggunakan pemegang variabel untuk memodifikasi nilai variabel lokal.

Mari kita lihat contoh yang menggunakan array untuk menyimpan variabel dalam aplikasi single-threaded:

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 berpikir bahwa streaming menjumlahkan 2 ke setiap nilai, tetapi sebenarnya menjumlahkan 0 karena ini adalah nilai terbaru yang tersedia saat lambda dijalankan.

Mari melangkah lebih jauh dan mengeksekusi jumlahnya di utas 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 rangkum di sini? Itu tergantung pada berapa lama pemrosesan simulasi kami. Jika cukup pendek untuk membiarkan eksekusi metode dihentikan sebelum utas lainnya dieksekusi, itu akan mencetak 6, jika tidak, itu akan mencetak 12.

Secara umum, jenis penyelesaian masalah ini rawan kesalahan dan dapat memberikan hasil yang tidak dapat diprediksi, jadi kita harus selalu menghindarinya.

6. Kesimpulan

Di artikel ini, kami telah menjelaskan mengapa ekspresi lambda hanya dapat menggunakan variabel lokal final atau final secara efektif. Seperti yang telah kita lihat, batasan ini berasal dari perbedaan sifat variabel-variabel ini dan bagaimana Java menyimpannya dalam memori. Kami juga telah menunjukkan bahaya menggunakan solusi umum.

Seperti biasa, kode sumber lengkap untuk contoh tersedia di GitHub.