Pada tipe nullable, akses method secara langsung akan ditolak oleh kompilator karena berisiko melempar NullPointerException. Kotlin menyediakan operator safe call ?. yang hanya memanggil method bila penerima tidak null; bila penerima null, seluruh ekspresi menghasilkan null tanpa melemparkan exception. Operator ini menjadi pondasi utama penanganan nullable di Kotlin dan dapat dirantaikan untuk traversal data yang dalam. Pada chapter ini kita akan membedah cara kerja ?., perantaian beberapa safe call, dan padanannya dalam bentuk if eksplisit.
Cara Kerja Operator ?. #
Operator ?. bekerja sebagai jembatan antara tipe nullable dan tipe non-nullable pada level property atau method. Bentuk umumnya adalah penerima?.anggota. Jika penerima bernilai null, ekspresi langsung menghasilkan null bertipe nullable. Jika penerima tidak null, anggota dipanggil secara normal. Padanannya dalam bentuk if adalah sebagai berikut:
// Bentuk aman dengan ?.
val panjang: Int? = nama?.length
// Padanan eksplisit
val panjang: Int? = if (nama != null) nama.length else nullKedua bentuk di atas ekuivalen secara semantik. Perbedaannya: bentuk ?. lebih ringkas, sedangkan bentuk if lebih eksplisit terlihat oleh pembaca. Kompilator menghasilkan bytecode yang sama untuk keduanya.
Perantaian Safe Call #
Kekuatan utama ?. terlihat saat kita merantaikan beberapa akses pada struktur data bertingkat. Misalnya pengguna?.alamat?.kota akan menghasilkan null bila salah satu titik di rantai tersebut null.
data class Pengguna(val nama: String, val alamat: Alamat?)
data class Alamat(val kota: String, val kodePos: String?)
fun main() {
val pengguna1: Pengguna? = Pengguna("Budi", Alamat("Bandung", "40123"))
val pengguna2: Pengguna? = null
val pengguna3: Pengguna? = Pengguna("Siti", null)
println("Nama pengguna1: ${pengguna1?.nama}")
println("Nama pengguna2: ${pengguna2?.nama}")
println("Nama pengguna3: ${pengguna3?.nama}")
println("Kota pengguna1: ${pengguna1?.alamat?.kota}")
println("Kota pengguna2: ${pengguna2?.alamat?.kota}")
println("Kota pengguna3: ${pengguna3?.alamat?.kota}")
println("Kode pos pengguna1: ${pengguna1?.alamat?.kodePos}")
println("Kode pos pengguna3: ${pengguna3?.alamat?.kodePos}")
val panjangNama: Int? = pengguna1?.nama?.length
println("Panjang nama pengguna1: $panjangNama")
val panjangNamaNull: Int? = pengguna2?.nama?.length
println("Panjang nama pengguna2: $panjangNamaNull")
val daftar: List<Pengguna?> = listOf(pengguna1, pengguna2, pengguna3)
for (p in daftar) {
val kota = p?.alamat?.kota
println("Pengguna=${p?.nama ?: "<tidak ada>"}, kota=${kota ?: "<tidak diketahui>"}")
}
val peta: Map<String, String?> = mapOf("kota" to "Bandung", "kodePos" to null)
val kode = peta["kodePos"]?.length
println("Panjang kode pos dari peta: $kode")
val hasil: String? = pengguna1?.alamat?.kota?.uppercase()
println("Kota pengguna1 huruf besar: $hasil")
}Output yang Dihasilkan:
Nama pengguna1: Budi
Nama pengguna2: null
Nama pengguna3: Siti
Kota pengguna1: Bandung
Kota pengguna2: null
Kota pengguna3: null
Kode pos pengguna1: 40123
Kode pos pengguna3: null
Panjang nama pengguna1: 4
Panjang nama pengguna2: null
Pengguna=Budi, kota=Bandung
Pengguna=<tidak ada>, kota=<tidak diketahui>
Pengguna=Siti, kota=<tidak diketahui>
Panjang kode pos dari peta: null
Kota pengguna1 huruf besar: BANDUNGMari kita uraikan baris-baris penting pada contoh di atas:
- Baris 1-2 (deklarasi
data class): classPenggunamemiliki propertyalamatyang nullable, dan classAlamatpunya propertykodePosnullable. Struktur bertingkat inilah yang membuat safe call chaining menjadi relevan. - Baris 6-8: tiga variabel
Pengguna?merepresentasikan tiga skenario — pengguna lengkap, penggunanull, dan pengguna tanpa alamat. Skenario ini mengekspos cabang null yang berbeda-beda. - Baris 12 (
pengguna1?.alamat?.kota): rantai dua safe call. Bilapengguna1null, hasilnya null. Bilapengguna1.alamatnull (seperti pada pengguna3), hasil juga null. Hanya bila keduanya tidak null,kotadiakses. - Baris 18 (
pengguna1?.nama?.length):namasendiri bertipe non-nullableString, namunpengguna1nullable membuat seluruh rantai menghasilkanInt?. - Baris 25 (
p?.nama ?: "<tidak ada>"): safe call pada iterasi list. Operator Elvis memberi teks pengganti bilap?.namamenghasilkannull. - Baris 30 (
peta["kodePos"]?.length): akses map via[]mengembalikan tipeV?(nullable value). Tambahan?.lengthmembuat hasilnyaInt?.
Kapan Safe Call Tidak Perlu Dipakai #
Bila variabel sudah non-nullable, kita tidak perlu ?. karena akses langsung sudah aman. Begitu pula bila sudah ada pengecekan != null sebelumnya, kompilator akan melakukan smart cast sehingga akses langsung diperbolehkan tanpa ?. — topik yang akan kita bahas pada chapter Smart Casts dan Null Checking.
Menggunakan ?. pada variabel non-nullable sebenarnya tidak ilegal — kompilator tetap mengizinkannya. Namun praktik tersebut dianggap code smell karena memberi sinyal palsu bahwa variabel bisa null, padahal kenyataannya tidak. Pembaca kode akan mengira ada potensi null yang harus diwaspadai, padahal compiler sudah menjamin kebalikannya. Karena itu, singkirkan ?. pada variabel yang sudah non-nullable agar kode lebih jujur terhadap kontrak tipenya.
Hasil Bertipe Nullable #
Salah satu konsekuensi penting dari ?. adalah tipe hasilnya selalu nullable. Ekspresi pengguna?.nama menghasilkan String? meskipun property nama sendiri bertipe String non-nullable. Hal ini masuk akal karena ?. harus mewakili kemungkinan penerima bernilai null. Bila kita ingin hasil non-nullable, operator Elvis ?: dapat dipakai untuk memberi nilai pengganti.
Pola umum pada kode produksi adalah mengombinasikan ?. dengan ?: pada akhir rantai. Misalnya pengguna?.alamat?.kota ?: "Tidak diketahui" menghasilkan String non-nullable yang pasti memiliki nilai — baik itu kota asli maupun teks pengganti. Pola ini menjaga alur logika tetap linier tanpa perlu bercabang ke if.
Referensi Lanjutan #
- Null-safety
— bagian safe call operator
?.pada panduan null-safety resmi
Pemahaman tentang ?. membuka pintu ke teknik lain: memberikan nilai default saat safe call menghasilkan null. Topik tersebut menjadi fokus chapter Elvis Operator.