Lambda boleh membaca variabel yang berada di sekitarnya — variabel lokal, parameter method, bahkan field objek. Kemampuan ini disebut closure: fungsi yang “membawa” konteks pembuatannya ke mana-mana, sehingga fungsi tetap bisa bekerja jauh setelah method pembuatnya selesai. Namun Java memasang satu syarat yang membuat programmer baru sering terkejut: variabel lokal yang ditangkap boleh dibaca, tetapi tidak boleh diubah — harus final atau effectively final. Pada Chapter ini kita membuktikan tiga hal dengan eksekusi nyata: nilai tangkapan disalin saat lambda dibuat, pelanggaran aturan berakhir di error kompilasi, dan cara idiomatis menghitung dari dalam lambda.
Nilai yang Ditangkap adalah Salinan #
Mari kita mulai dari bukti paling mendasar: lambda tidak menyimpan tautan ke variabel lokal, melainkan salinan nilainya pada saat lambda dibuat. Method buatSapaan berikut mengembalikan lambda yang membaca parameter nama; setelah method selesai, parameter itu secara teknis sudah tidak hidup, tetapi lambda tetap ingat isinya:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class TangkapNilai {
static Supplier<String> buatSapaan(String nama) {
// parameter nama disalin ke dalam lambda saat lambda DIBUAT
return () -> "Halo, " + nama + "!";
}
public static void main(String[] args) {
// Dua lambda menangkap nilai yang berbeda
Supplier<String> sapaBudi = buatSapaan("Budi");
Supplier<String> sapaSiti = buatSapaan("Siti");
// Dipanggil setelah parameter nama selesai hidup
System.out.println(sapaBudi.get());
System.out.println(sapaSiti.get());
// Setiap iterasi membuat salinan tersendiri untuk lambda
List<Supplier<Integer>> lonceng = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
int salinan = i; // variabel baru tiap iterasi, effectively final
lonceng.add(() -> salinan * 10);
}
for (Supplier<Integer> s : lonceng) {
System.out.println("lonceng: " + s.get());
}
}
}Output yang Dihasilkan:
Halo, Budi!
Halo, Siti!
lonceng: 10
lonceng: 20
lonceng: 30Tiga pengamatan dari hasil eksekusi tersebut:
- Baris 6-8 (
return () -> "Halo, " + nama + "!";): lambda dibuat di dalam method, lalu lolos keluar dari method lewat nilai kembalian. JVM menyimpan salinannamadi dalam objek lambda — karena itu lambda tetap berfungsi setelahbuatSapaanselesai dan parameternya tidak lagi bisa diakses. - Baris 14-15 (
sapaBudidansapaSiti): dua panggilan method yang sama menghasilkan dua lambda dengan tangkapan berbeda —"Budi"dan"Siti". Satu baris kode, dua konteks berbeda; inilah yang dimaksud fungsi “menutup” variabel lingkungannya. - Baris 21-28 (loop dengan
int salinan = i;): variabel loopiberubah-ubah sehingga tidak boleh ditangkap langsung. Trikint salinan = imembuat variabel baru pada setiap iterasi — variabel yang masing-masing effectively final — dan setiap lambda menangkap salinannya sendiri: hasilnya10,20,30, bukan30tiga kali. Setiap salinan dibuat saat lambda dibuat, bukan saat lambda dipanggil.
Saat Pelanggaran Terjadi #
Sekarang sengaja kita langgar. Program berikut mencoba menaikkan variabel lokal jumlah dari dalam Consumer yang dijalankan forEach:
import java.util.List;
import java.util.function.Consumer;
public class PenghitungGagal {
public static void main(String[] args) {
List<String> nama = List.of("Budi", "Siti", "Dewi");
int jumlah = 0;
Consumer<String> hitung = n -> {
System.out.println("Memproses " + n);
jumlah++; // variabel lokal diubah dari dalam lambda
};
nama.forEach(hitung);
System.out.println("Total: " + jumlah);
}
}Berkas tersebut ditolak compiler javac sebelum sempat berjalan, dengan pesan persis seperti ini:
PenghitungGagal.java:11: error: local variables referenced from a lambda
expression must be final or effectively finalPesan ini menyebut dua pilihan: final (kata kunci eksplisit) atau effectively final — variabel tanpa kata kunci final yang sejak inisialisasi tidak pernah diberi nilai baru. Compiler menganalisis seluruh method: satu saja penugasan ulang (jumlah++ sama artinya dengan jumlah = jumlah + 1) membatalkan status effectively final, dan variabel itu langsung dilarang masuk lambda.
Mengapa Java seketat itu? Dua alasan. Pertama, race condition: lambda bisa berjalan di thread lain atau jauh setelah method selesai, sementara variabel lokal hidup di stack frame method — salinan satu nilai aman dibawa, sedangkan tautan hidup ke stack tidak mungkin. Kedua, kejelasan: jika lambda bisa mengubah variabel luar, urutan eksekusi forEach atau stream paralel akan mengubah hasil program — jenis nondeterminisme yang sulit dijejak. Dengan aturan ini, tangkapan selalu bersifat nilai yang dibekukan.
Solusi yang Idiomatis #
Larangan menugaskan ulang variabel tidak berarti penghitung mustahil. Kita pindahkan angka yang berubah ke dalam objek — dan AtomicInteger dari paket java.util.concurrent.atomic adalah wadah standarnya:
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class PenghitungAman {
public static void main(String[] args) {
List<String> nama = List.of("Budi", "Siti", "Dewi");
// Referensi jumlah tetap efektif final; ISI objeknya boleh berubah
AtomicInteger jumlah = new AtomicInteger(0);
nama.forEach(n -> {
System.out.println("Memproses " + n);
jumlah.incrementAndGet();
});
System.out.println("Total: " + jumlah.get());
}
}Output yang Dihasilkan:
Memproses Budi
Memproses Siti
Memproses Dewi
Total: 3Bedah solusinya:
- Baris 10 (
AtomicInteger jumlah = new AtomicInteger(0);): variabeljumlahhanya diinisialisasi sekali dan tidak pernah ditugaskan ulang — statusnya effectively final, sah ditangkap lambda. Yang berubah adalah isi objek yang ditunjuknya, bukan variabelnya. - Baris 13-16 (
jumlah.incrementAndGet()): methodincrementAndGet()menaikkan nilai secara atomik dan mengembalikan nilai baru. Pada stream paralel atau multi-thread, operasi atomik ini bebas race condition — alasan utamaAtomicIntegerdipilih ketimbangint[]sederhana. - Baris 19 (
jumlah.get()): nilai akhir dibaca setelah seluruhforEachselesai. Keluarannya3, tepat jumlah nama yang diproses — perilaku yang gagal dicapai berkas sebelumnya. - Alternatif sederhana: wadah array satu elemen (
int[] jumlah = {0};lalujumlah[0]++) bekerja untuk kode satu thread karena elemen array boleh diubah. Untuk kode yang berpeluang paralel, tetap pilihAtomicInteger.
Aturan effectively final menutup satu sisi gelap gaya fungsional: lambda yang tidak diam-diam mengubah dunia sekitarnya. Sisi gelap satunya lagi adalah side effect secara umum — method yang mengubah list luar, mencetak, atau menulis berkas. Chapter berikutnya membedah garis batas antara pure function yang mudah diuji dan fungsi ber-efek samping yang harus dikelola dengan sadar.
Referensi #
- dev.java: Lambda Expressions
- Oracle: java.util.concurrent.atomic.AtomicInteger
- Java Language Specification (JLS) — Bab 15
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PenghitungAman.java
Unduh berkasimport java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class PenghitungAman {
public static void main(String[] args) {
List<String> nama = List.of("Budi", "Siti", "Dewi");
// Referensi jumlah tetap efektif final; ISI objeknya boleh berubah
AtomicInteger jumlah = new AtomicInteger(0);
nama.forEach(n -> {
System.out.println("Memproses " + n);
jumlah.incrementAndGet();
});
System.out.println("Total: " + jumlah.get());
}
}
PenghitungGagal.java
Unduh berkasimport java.util.List;
import java.util.function.Consumer;
public class PenghitungGagal {
public static void main(String[] args) {
List<String> nama = List.of("Budi", "Siti", "Dewi");
int jumlah = 0;
Consumer<String> hitung = n -> {
System.out.println("Memproses " + n);
jumlah++; // variabel lokal diubah dari dalam lambda
};
nama.forEach(hitung);
System.out.println("Total: " + jumlah);
}
}
TangkapNilai.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class TangkapNilai {
static Supplier<String> buatSapaan(String nama) {
// parameter nama disalin ke dalam lambda saat lambda DIBUAT
return () -> "Halo, " + nama + "!";
}
public static void main(String[] args) {
// Dua lambda menangkap nilai yang berbeda
Supplier<String> sapaBudi = buatSapaan("Budi");
Supplier<String> sapaSiti = buatSapaan("Siti");
// Dipanggil setelah parameter nama selesai hidup
System.out.println(sapaBudi.get());
System.out.println(sapaSiti.get());
// Setiap iterasi membuat salinan tersendiri untuk lambda
List<Supplier<Integer>> lonceng = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
int salinan = i; // variabel baru tiap iterasi, effectively final
lonceng.add(() -> salinan * 10);
}
for (Supplier<Integer> s : lonceng) {
System.out.println("lonceng: " + s.get());
}
}
}