lateinit dan lazy

Pola inisialisasi yang ditunda dengan lateinit dan lazy.

Daftar isi

Tipe nullable menuntut setiap pemakaian ditangani, dan ketentuan itu terasa berat untuk properti yang sebenarnya pasti terisi, hanya saja pengisiannya menunggu waktu yang tepat. Kotlin menyediakan dua pola inisialisasi yang ditunda tanpa menjadikan properti nullable, yaitu modifier lateinit pada properti kelas dan fungsi lazy untuk nilai yang dihitung sekali. Topik ini membahas cara kerja keduanya, risikonya, dan cara memilih di antara keduanya.

Modifier lateinit dipasang pada properti var bertipe non-null untuk menunda pengisiannya, sementara compiler dijanjikan bahwa properti tersebut terisi sebelum dipakai. Bentuk ini lazim pada properti kelas yang nilainya disiapkan setelah objek dibuat, misalnya data yang dimuat dari sumber luar. Properti semacam itu tidak perlu dinyatakan nullable, sehingga pemakaiannya setelah terisi bebas dari penanganan null. Program berikut memuat data ke dalam kelas statistik sebelum nilainya dihitung.

Buat berkas main.kt berikut.

kotlin main.kt
class StatistikNilai {
    lateinit var data: List<Int>

    fun muat(nilai: List<Int>) {
        data = nilai
    }

    fun rataRata(): Double {
        return data.average()
    }

    fun sudahDiisi(): Boolean {
        return ::data.isInitialized
    }
}

fun main() {
    val statistik = StatistikNilai()
    statistik.muat(listOf(70, 80, 90))
    println("Rata-rata: ${statistik.rataRata()}")
    println("Sudah diisi: ${statistik.sudahDiisi()}")

    val kosong = StatistikNilai()
    println("Sudah diisi: ${kosong.sudahDiisi()}")
}

Dua baris export pada perintah berikut mengarahkan terminal ke compiler Kotlin yang dipakai jalur ini, dan jalurnya perlu disesuaikan dengan lokasi instalasi pada komputer lain.

bash
# compiler Kotlin dan pustaka standar yang dipakai jalur ini
export PATH=/home/kal/my-project/tools/kotlinc/bin:/home/kal/my-project/tools/jdk-merged/bin:$PATH
export STDLIB=/home/kal/my-project/tools/kotlinc/lib/kotlin-stdlib.jar
# kompilasi berkas main.kt menjadi out.jar, lalu jalankan programnya
kotlinc main.kt -d out.jar && java -cp "out.jar:$STDLIB" MainKt
text Keluaran
Rata-rata: 80.0
Sudah diisi: true
Sudah diisi: false

Properti data dideklarasikan lateinit var bertipe List<Int>, sehingga tidak perlu diisi pada saat objek dibuat dan tidak perlu dinyatakan List<Int>?. Metode muat mengisinya, dan setelah itu pemanggilan data.average() berjalan tanpa pemeriksaan apa pun. Fungsi sudahDiisi menunjukkan cara menanyakan keadaan properti, yaitu ::data.isInitialized yang menghasilkan true atau false tanpa menyentuh nilainya. Keluaran memperlihatkan keduanya, yaitu true untuk objek yang datanya sudah dimuat dan false untuk objek yang baru dibuat. Pemeriksaan ini berguna ketika status pengisian memang perlu ditanyakan, misalnya sebelum menampilkan ringkasan.

Alternatif tanpa lateinit adalah mendeklarasikan data sebagai List<Int>? yang diisi null pada awalnya. Setiap pemakaiannya kemudian menuntut safe call atau pemeriksaan, padahal kekosongannya memang tidak dimaksudkan sebagai keadaan yang sah. Modifier lateinit menyatakan niat itu kepada compiler, sehingga pemakaiannya setelah terisi sama bersihnya dengan properti biasa.

Akses sebelum inisialisasi#

Janji pada lateinit tidak diperiksa compiler, sehingga pemanggilan sebelum pengisian baru terungkap pada waktu program berjalan. Berkas berikut mengakses properti yang belum pernah diisi.

kotlin belum.kt
class JadwalPiket {
    lateinit var petugas: String
}

fun main() {
    val jadwal = JadwalPiket()
    println(jadwal.petugas)
}
bash
# kompilasi berkas belum.kt, lalu jalankan dengan keluaran dibatasi empat baris pertama
kotlinc belum.kt -d belum.jar && java -cp "belum.jar:$STDLIB" BelumKt 2>&1 | head -4
text Keluaran
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property petugas has not been initialized
	at JadwalPiket.getPetugas(belum.kt:2)
	at BelumKt.main(belum.kt:7)
	at BelumKt.main(belum.kt)

Program berhenti dengan UninitializedPropertyAccessException, dan pesannya menyebut nama properti yang belum diisi, sehingga penyebab kegagalannya langsung terbaca. Empat baris pertama dari stack trace ditampilkan agar keluarannya ringkas. Pesan semacam ini lebih informatif daripada NullPointerException biasa karena menunjuk propertinya, tetapi programnya tetap terhenti. Konsekuensinya, lateinit hanya layak dipakai bila urutan pengisian dikendalikan dengan pasti, misalnya oleh siklus hidup yang jelas pada kelas itu sendiri. Bila kepastian itu tidak ada, tanyakan keadaannya dengan isInitialized terlebih dahulu, atau nyatakan propertinya sebagai nullable dengan penanganan yang tertulis.

Inisialisasi sekali dengan lazy#

Fungsi lazy bekerja pada sisi berlawanan, yaitu menunda perhitungan nilai val hingga nilai itu pertama kali dipakai. Bentuknya by lazy { }, dengan blok yang dijalankan hanya pada akses pertama dan hasilnya disimpan untuk pemakaian berikutnya. Program berikut menyisipkan println di dalam blok lazy sebagai bukti bahwa perhitungannya hanya berlangsung sekali.

kotlin lazy.kt
fun main() {
    val dataUjian = listOf(70, 80, 90)

    val rataRata: Double by lazy {
        println("Rata-rata dihitung")
        dataUjian.average()
    }

    println("Program dimulai")
    println("Rata-rata: $rataRata")
    println("Rata-rata lagi: $rataRata")
}
bash
# kompilasi berkas lazy.kt menjadi lazy.jar, lalu jalankan programnya
kotlinc lazy.kt -d lazy.jar && java -cp "lazy.jar:$STDLIB" LazyKt
text Keluaran
Program dimulai
Rata-rata dihitung
Rata-rata: 80.0
Rata-rata lagi: 80.0

Keluaran memperlihatkan urutan yang penting. Pesan Rata-rata dihitung muncul setelah Program dimulai, yang berarti blok lazy tidak dijalankan pada saat deklarasi, melainkan pada akses pertama. Pada pemakaian kedua, pesan tersebut tidak muncul lagi, sehingga nilai cukup diambil dari yang tersimpan. Pola ini menghemat pekerjaan ketika perhitungannya mahal dan tidak selalu diperlukan, misalnya menghitung statistik dari daftar nilai yang panjang. Kata kunci by di antara properti dan lazy { } merupakan bentuk delegasi, yaitu mekanisme penyerahan pekerjaan pengisian properti kepada objek lain.

Tipe yang dihasilkan bloknya bebas, mulai dari angka, teks, hingga objek yang kompleks, dan lazy juga sah untuk nilai bertipe nullable meskipun pemakaiannya paling cocok untuk nilai yang pasti terisi setelah dihitung. Yang mengikat adalah sifat val-nya, yaitu hasil pertama tersimpan selamanya dan bloknya tidak pernah dijalankan ulang.

Memilih antara lateinit dan lazy#

Kedua pola menjawab kebutuhan yang berbeda, dan tabel berikut merangkum perbedaannya.

Aspeklateinitlazy
Properti yang dipakaivarval
Sumber nilaidiisi dari luar oleh kode laindihitung sendiri dari data yang ada
Pekerjaan yang ditundapenugasan nilai oleh kode lainperhitungan nilai pada akses pertama
Bila dipakai terlalu diniUninitializedPropertyAccessExceptionperhitungan dijalankan saat itu juga

lateinit dipilih ketika nilainya berasal dari luar kelas dan pengisiannya bergantung pada urutan kejadian, sedangkan lazy dipilih ketika nilai dapat dihitung sendiri dari data yang sudah ada. Properti lateinit mempertahankan sifat var-nya, sehingga nilainya boleh diganti sepanjang usia objek, sedangkan properti lazy berupa val yang diamankan sejak akses pertamanya. Pada keduanya, properti tidak perlu dinyatakan nullable, sehingga kode pemakainya bebas dari penanganan null. Apabila urutan pengisiannya tidak dapat dijamin dan nilainya tidak dapat dihitung sendiri, nilai nullable dengan penanganan yang tertulis tetap menjadi pilihan yang paling aman.

Pada praktiknya, lateinit sering ditemui pada properti yang pengisiannya dikelola framework, sedangkan lazy sering dipakai untuk konfigurasi atau hasil perhitungan yang berat tetapi jarang dipakai. Keduanya menghilangkan kebutuhan variabel sementara yang harus diisi secara manual pada saat objek dibuat.

Ringkasan#

  • lateinit menunda pengisian properti var bertipe non-null, dan keadaannya dapat ditanyakan dengan isInitialized.
  • Mengakses properti lateinit sebelum diisi menghentikan program dengan UninitializedPropertyAccessException yang menyebut nama propertinya.
  • by lazy { } menunda perhitungan nilai val hingga akses pertama, lalu menyimpan hasilnya untuk pemakaian berikutnya.
  • lateinit untuk nilai dari luar yang pengisiannya bergantung pada urutan kejadian, sedangkan lazy untuk nilai yang dihitung sendiri sekali.

Referensi#