Closure adalah lambda yang menangkap variabel dari lingkup luar tempat lambda didefinisikan. Variabel yang ditangkap tetap hidup selama lambda itu hidup, bahkan setelah lingkup aslinya selesai. Pada chapter ini kita akan membedah dua jenis closure — read-only (menangkap val) dan mutable (menangkap var) — serta implikasi lifecycle yang muncul saat lambda disimpan lebih lama dari function yang mendefinisikannya. Kita juga akan melihat bagaimana closure memungkinkan pola function factory yang menyimpan state antar pemanggilan.
Apa Itu Closure #
Closure terbentuk ketika sebuah lambda mengakses variabel dari lingkup luar. Kotlin secara otomatis membuat capture — snapshot referensi ke variabel tersebut. Variabel yang ditangkap tetap dapat diakses dari dalam lambda walaupun lingkup aslinya sudah selesai.
val faktor = 10
val kalikanFaktor: (Int) -> Int = { n -> n * faktor }
// variabel `faktor` ditangkap dari luar lambda ↑Lambda kalikanFaktor menangkap faktor dari lingkup luar. Bila faktor adalah val, lambda mengakses nilai yang sama setiap pemanggilan. Bila faktor adalah var, lambda mengakses nilai terbaru saat lambda dipanggil.
Read-Only vs Mutable Closure #
Mari kita lihat perbedaan closure yang menangkap val vs var, serta pola factory yang memanfaatkan mutable closure.
// closures.kt
// Closure: lambda yang menangkap variabel dari lingkup luar.
fun main() {
// Read-only closure: menangkap val.
val faktor = 10
val kalikanFaktor: (Int) -> Int = { n -> n * faktor }
println("5 * faktor(10) = ${kalikanFaktor(5)}")
// Mutable closure: menangkap var.
var jumlah = 0
val tambahKe = { n: Int ->
jumlah += n
jumlah
}
println("tambahKe(5) -> ${tambahKe(5)}")
println("tambahKe(10) -> ${tambahKe(10)}")
println("tambahKe(3) -> ${tambahKe(3)}")
println("Total jumlah akhir: $jumlah")
// Closure yang menangkap list mutable.
val log = mutableListOf<String>()
val catat: (String) -> Unit = { pesan ->
log.add(pesan)
}
catat("memulai program")
catat("memuat konfigurasi")
catat("menjalankan tugas")
println("Log tercatat: ${log.size} baris")
log.forEach { println(" - $it") }
// Closure mengembalikan function yang menyimpan state.
fun pembuatPenghitung(): () -> Int {
var hitung = 0
return { hitung++; hitung }
}
val hitung1 = pembuatPenghitung()
println("hitung1() = ${hitung1()}")
println("hitung1() = ${hitung1()}")
println("hitung1() = ${hitung1()}")
val hitung2 = pembuatPenghitung()
println("hitung2() = ${hitung2()}")
// Capturing by reference — perubahan var tercermin di lambda.
var nilai = 100
val ambilNilai: () -> Int = { nilai }
println("Ambil nilai: ${ambilNilai()}")
nilai = 200
println("Ambil nilai setelah ubah: ${ambilNilai()}")
}Output yang Dihasilkan:
5 * faktor(10) = 50
tambahKe(5) -> 5
tambahKe(10) -> 15
tambahKe(3) -> 18
Total jumlah akhir: 18
Log tercatat: 3 baris
- memulai program
- memuat konfigurasi
- menjalankan tugas
hitung1() = 1
hitung1() = 2
hitung1() = 3
hitung2() = 1
Ambil nilai: 100
Ambil nilai setelah ubah: 200Mari kita telaah baris-baris kode di atas:
- Baris 7-9 (
val faktor = 10; val kalikanFaktor = { n -> n * faktor }): read-only closure. Lambda menangkapval faktor. Nilaifaktortidak dapat diubah, sehingga lambda selalu memakai nilai10. - Baris 12-17 (
var jumlah = 0; val tambahKe = { ... jumlah += n ... }): mutable closure. Lambda memodifikasivar jumlahdari dalam body. Setiap pemanggilantambahKemenambah dan mengembalikan nilaijumlahterbaru — perubahan terlihat dari luar. - Baris 20-23 (
val log = mutableListOf<String>(); val catat = { pesan -> log.add(pesan) }): closure menangkap collection mutable. Lambdacatatmenambah elemen ke list yang sama setiap kali dipanggil. List tetap dapat diakses dari luar. - Baris 28-32 (
fun pembuatPenghitung(): () -> Int { var hitung = 0; return { ... } }): function factory yang menyimpan state. Setiap pemanggilanpembuatPenghitung()membuat instancevar hitungbaru dan mengembalikan lambda yang menangkapnya. Lambdahitung1danhitung2memiliki state terpisah. - Baris 39-43 (
val hitung1 = pembuatPenghitung(); ...): tiga pemanggilanhitung1()menghasilkan1, 2, 3— statehitungdipertahankan antar pemanggilan.hitung2()dimulai dari1kembali karena state-nya berbeda. - Baris 46-50 (
var nilai = 100; val ambilNilai = { nilai }; nilai = 200; ... ambilNilai()): closure menangkap referensi kevar, bukan snapshot nilai. Setelahnilaidiubah menjadi200, pemanggilanambilNilai()mengembalikan nilai terbaru200.
Implikasi Lifecycle #
Closure memperpanjang hidup variabel yang ditangkap. Variabel tidak akan di-garbage collected selama lambda masih hidup. Hal ini penting saat lambda disimpan dalam cache, listener, atau callback async — variabel yang ditangkap ikut tertahan.
| Situasi | Konsekuensi |
|---|---|
| Lambda lokal dipanggil sinkron | Variabel tertahan sebentar, lalu dilepas |
| Lambda disimpan di listener | Variabel tertahan selama listener aktif |
| Lambda di coroutine async | Variabel tertahan sampai coroutine selesai |
Closure menangkap Activity/Context | Risiko memory leak pada Android |
Pola Factory dengan Closure #
Closure memungkinkan pola function factory — function yang mengembalikan function baru dengan state tertentu. Setiap pemanggilan factory membuat instance state terpisah. Pola ini sering dipakai untuk membuat counter, accumulator, atau throttler yang dapat dikonfigurasi.
fun pembuatPenambah(basis: Int): (Int) -> Int = { n -> n + basis }
val tambah10 = pembuatPenambah(10)
val tambah100 = pembuatPenambah(100)
// tambah10 dan tambah100 punya state basis terpisahReferensi Lanjutan #
- Higher-order functions and lambdas — panduan closure sebagai lambda yang menangkap variabel
- Inline functions — dampak inlining pada closure
Penutup #
Closure memberi lambda kemampuan “mengingat” lingkup tempat lambda didefinisikan. Dengan memahami perbedaan read-only vs mutable closure dan implikasi lifecycle, kita dapat memakai closure untuk membangun factory, counter, dan callback dengan aman. Selanjutnya, kita akan melihat bagaimana Kotlin mengoptimalkan lambda via inline function — fitur yang mengurangi overhead alokasi lambda tanpa mengubah perilaku semantik.