Pada tipe Any atau Any?, kita sering perlu mengonversi ke tipe yang lebih spesifik. Operator cast as di Kotlin bersifat unsafe — bila gagal, ia melempar ClassCastException. Untuk menangani tipe yang tidak pasti secara aman, Kotlin menyediakan operator safe cast as? yang mengembalikan null saat konversi tidak memungkinkan. Pada chapter ini kita akan membedah cara kerja as?, perbedaannya dengan as, serta pola umum kombinasi as? dengan operator Elvis dan mapNotNull.

Perbedaan as dan as? #

OperatorNamaSaat GagalTipe KembalianCocok untuk
asUnsafe castMelempar ClassCastExceptionT (non-nullable)Tipe sudah pasti benar
as?Safe castMengembalikan nullT? (nullable)Tipe mungkin tidak cocok

Operator as sebaiknya dipakai hanya bila kompilator membuktikan tipe target pasti benar, atau bila kita ingin program gagal cepat (fail-fast) saat asumsi dilanggar. Operator as? lebih disukai saat data berasal dari sumber yang tidak terkontrol seperti JSON, database, atau library Java.

Cara Kerja as? #

Operator as? menguji apakah nilai dapat dikonversi ke tipe target. Bila ya, hasilnya nilai tersebut dengan tipe target. Bila tidak, hasilnya null bertipe nullable. Operator ini tidak pernah melempar exception karena penanganan ketidakcocokan ditangani via nilai null.

kotlin
fun main() {
    val objek1: Any = "Halo, Kotlin"
    val teks1: String? = objek1 as? String
    println("Hasil as? String: $teks1, panjang=${teks1?.length}")

    val objek2: Any = 42
    val teks2: String? = objek2 as? String
    println("Hasil 42 as? String: $teks2")

    val objek3: Any = listOf(1, 2, 3)
    val daftar: List<*>? = objek3 as? List<*>
    println("Hasil as? List: $daftar, ukuran=${daftar?.size}")

    val objek4: Any = mapOf("kota" to "Bandung")
    val peta: Map<*, *>? = objek4 as? Map<*, *>
    println("Hasil as? Map: $peta")

    val angka: Any = 100L
    val sebagaiInt: Int? = angka as? Int
    val sebagaiLong: Long? = angka as? Long
    println("100L as? Int: $sebagaiInt")
    println("100L as? Long: $sebagaiLong")

    val daftarCampuran: List<Any> = listOf("teks", 42, 3.14, true)
    val hanyaString: List<String> = daftarCampuran.mapNotNull { it as? String }
    println("Hanya string: $hanyaString")

    val hanyaAngka: List<Int> = daftarCampuran.mapNotNull { it as? Int }
    println("Hanya angka Int: $hanyaAngka")

    val tipeTidakDiketahui: Any = "Budi"
    val panjangNama: Int? = (tipeTidakDiketahui as? String)?.length
    println("Panjang nama: $panjangNama")

    val input: Any = 12345
    val hasil: String = (input as? String) ?: "<bukan string>"
    println("Hasil dengan elvis: $hasil")

    val nullEksplisit: Any? = null
    val ujiNull: String? = nullEksplisit as? String
    println("null as? String: $ujiNull")

    try {
        val salah: String = objek2 as String
        println(salah)
    } catch (e: ClassCastException) {
        println("ClassCastException: ${e.message?.take(60)}")
    }
}

Output yang Dihasilkan:

text
Hasil as? String: Halo, Kotlin, panjang=12
Hasil 42 as? String: null
Hasil as? List: [1, 2, 3], ukuran=3
Hasil as? Map: {kota=Bandung}
100L as? Int: null
100L as? Long: 100
Hanya string: [teks]
Hanya angka Int: [42]
Panjang nama: 4
Hasil dengan elvis: <bukan string>
null as? String: null
ClassCastException: class java.lang.Integer cannot be cast to class java.lang.St

Mari kita uraikan baris-baris penting pada contoh di atas:

  1. Baris 3 (objek1 as? String): objek1 berisi "Halo, Kotlin" yang merupakan String. Cast berhasil dan teks1 bertipe String? berisi nilai tersebut.
  2. Baris 7 (objek2 as? String): objek2 berisi 42 (Int), bukan String. Cast gagal secara diam-diam, hasilnya null. Tidak ada exception yang dilempar.
  3. Baris 10 (objek3 as? List<*>): safe cast ke tipe generic. List<*> (star projection) menyatakan tipe elemen tidak diketahui, namun struktur list itu sendiri tetap dapat dipakai.
  4. Baris 17-19 (angka as? Int vs angka as? Long): tipe angka tidak saling kompatibel meskipun secara numerik bernilai sama. 100L adalah Long, sehingga as? Int menghasilkan null. Bila ingin konversi numerik, gunakan method toInt() atau toLong(), bukan cast.
  5. Baris 22 (daftarCampuran.mapNotNull { it as? String }): pola paling sering dipakai bersama as?. mapNotNull menerapkan lambda ke setiap elemen dan menyaring null. Hasilnya list hanya berisi elemen yang berhasil di-cast.
  6. Baris 28 ((tipeTidakDiketahui as? String)?.length): kombinasi safe cast dengan safe call. Bila cast gagal, hasilnya null; bila cast berhasil, .length dipanggil. Rantai ini sangat umum saat memproses data dinamis.
  7. Baris 32 ((input as? String) ?: "<bukan string>"): kombinasi safe cast dengan operator Elvis. Hasil akhir selalu non-nullable String, dengan nilai pengganti yang jelas bila cast gagal.
  8. Baris 36 (nullEksplisit as? String): bila nilai aslinya null, safe cast mengembalikan null alih-alih melempar NullPointerException — perilaku yang konsisten dengan filosofi null-safety Kotlin.
  9. Baris 39-43 (objek2 as String dalam try-catch): sebagai pembanding, unsafe cast as langsung melempar ClassCastException saat gagal. Inilah alasan as? lebih disukai kecuali kita yakin tipe benar-benar cocok.

Referensi Lanjutan #

  • Smart casts — bagian safe cast as? pada panduan typecasts resmi
  • Type casts — panduan resmi unsafe cast as dan safe cast as?

Setelah memahami as?, langkah berikutnya adalah mengenal fitur smart cast yang memungkinkan kompilator menyesuaikan tipe secara otomatis setelah pengecekan is — topik yang akan kita bahas pada chapter Smart Casts.