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.

kotlin Bentuk dasar closure
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.

kotlin
// 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:

text
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: 200

Mari kita telaah baris-baris kode di atas:

  1. Baris 7-9 (val faktor = 10; val kalikanFaktor = { n -> n * faktor }): read-only closure. Lambda menangkap val faktor. Nilai faktor tidak dapat diubah, sehingga lambda selalu memakai nilai 10.
  2. Baris 12-17 (var jumlah = 0; val tambahKe = { ... jumlah += n ... }): mutable closure. Lambda memodifikasi var jumlah dari dalam body. Setiap pemanggilan tambahKe menambah dan mengembalikan nilai jumlah terbaru — perubahan terlihat dari luar.
  3. Baris 20-23 (val log = mutableListOf<String>(); val catat = { pesan -> log.add(pesan) }): closure menangkap collection mutable. Lambda catat menambah elemen ke list yang sama setiap kali dipanggil. List tetap dapat diakses dari luar.
  4. Baris 28-32 (fun pembuatPenghitung(): () -> Int { var hitung = 0; return { ... } }): function factory yang menyimpan state. Setiap pemanggilan pembuatPenghitung() membuat instance var hitung baru dan mengembalikan lambda yang menangkapnya. Lambda hitung1 dan hitung2 memiliki state terpisah.
  5. Baris 39-43 (val hitung1 = pembuatPenghitung(); ...): tiga pemanggilan hitung1() menghasilkan 1, 2, 3 — state hitung dipertahankan antar pemanggilan. hitung2() dimulai dari 1 kembali karena state-nya berbeda.
  6. Baris 46-50 (var nilai = 100; val ambilNilai = { nilai }; nilai = 200; ... ambilNilai()): closure menangkap referensi ke var, bukan snapshot nilai. Setelah nilai diubah menjadi 200, pemanggilan ambilNilai() mengembalikan nilai terbaru 200.

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.

SituasiKonsekuensi
Lambda lokal dipanggil sinkronVariabel tertahan sebentar, lalu dilepas
Lambda disimpan di listenerVariabel tertahan selama listener aktif
Lambda di coroutine asyncVariabel tertahan sampai coroutine selesai
Closure menangkap Activity/ContextRisiko 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.

kotlin Factory dengan closure
fun pembuatPenambah(basis: Int): (Int) -> Int = { n -> n + basis }

val tambah10 = pembuatPenambah(10)
val tambah100 = pembuatPenambah(100)
// tambah10 dan tambah100 punya state basis terpisah

Referensi Lanjutan #

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.