Generics di Kotlin berjalan di atas JVM, dan JVM menerapkan type erasure — informasi tipe generic dihapus saat kompilasi. Artinya, List<String> dan List<Int> sama-sama menjadi List di runtime, tanpa jejak tipe elemen. Konsekuensinya, kita tidak dapat melakukan pemeriksaan seperti if (x is List<String>) karena informasi String sudah hilang. Pada chapter ini kita akan memahami cara kerja type erasure, melihat apa yang masih bisa dan tidak bisa dilakukan saat runtime, serta menyiapkan dasar untuk reified type parameters yang menjadi solusi sebagian dari batasan ini.

Apa Itu Type Erasure #

Type erasure berarti kompilator memakai informasi tipe generic saat kompilasi untuk memverifikasi type safety, tetapi menghapus informasi tersebut dari bytecode yang dihasilkan. Pada runtime, hanya tipe raw (misalnya List) yang tersisa, bukan tipe generic penuh (List<String>).

kotlin List<String> dan List<Int> sama di runtime
val daftarString: List<String> = listOf("A", "B")
val daftarInt: List<Int> = listOf(1, 2)
// Di runtime, keduanya hanya "List"

Alasan historis type erasure adalah kompatibilitas mundur dengan kode Java lama yang ditulis sebelum generics diperkenalkan. Karena JVM tetap menjalankan bytecode lama, generics ditambahkan sebagai fitur compile-time tanpa mengubah format bytecode.

Apa yang Bisa dan Tidak Bisa Dilakukan #

Type erasure membatasi apa yang dapat kita lakukan saat runtime:

OperasiBisa?Catatan
x is List<*>YaHanya cek tipe raw
x is List<String>TidakInformasi String hilang
x::classYaMengembalikan kelas raw (mis. ArrayList)
elemen is String saat iterasiYaCek tipe elemen individual
Membuat array Array<T> genericTidakHarus reified atau Array<Any?>

Demonstrasi Type Erasure #

Mari kita lihat bagaimana type erasure memengaruhi kode yang kita tulis:

kotlin
// Chapter: Type Erasure
// Informasi tipe generic hilang saat runtime (erased).

fun cetakTipeRuntime(item: Any) {
    println("Tipe runtime: ${item::class.simpleName}")
}

fun prosesList(daftar: List<Int>) {
    var total = 0
    for (n in daftar) total += n
    println("Total: $total")
}

fun main() {
    val daftarInt: List<Int> = listOf(1, 2, 3)
    val daftarStr: List<String> = listOf("A", "B", "C")

    cetakTipeRuntime(daftarInt)
    cetakTipeRuntime(daftarStr)

    // Cek kelas runtime: List<Int> dan List<String> sama persis di runtime
    println("daftarInt::class = ${daftarInt::class}")
    println("daftarStr::class = ${daftarStr::class}")
    println("Apakah sama? ${daftarInt::class == daftarStr::class}")

    // is List<*>
    val x: Any = daftarInt
    if (x is List<*>) {
        println("x adalah List dengan ukuran ${x.size}")
    }

    // Hanya bisa mengecek tipe elemen dengan is saat iterasi
    val campur: List<Any> = listOf(1, "dua", 3.0, "empat")
    for (e in campur) {
        when (e) {
            is Int -> println("$e adalah Int")
            is String -> println("$e adalah String")
            is Double -> println("$e adalah Double")
            else -> println("$e tipe lain")
        }
    }

    prosesList(daftarInt)
}

Output yang Dihasilkan:

text
Tipe runtime: ArrayList
Tipe runtime: ArrayList
daftarInt::class = class java.util.Arrays$ArrayList
daftarStr::class = class java.util.Arrays$ArrayList
Apakah sama? true
x adalah List dengan ukuran 3
1 adalah Int
dua adalah String
3.0 adalah Double
empat adalah String
Total: 6

Mari kita telaah baris-baris kode di atas:

  1. fun cetakTipeRuntime(item: Any): function menerima Any lalu mencetak nama kelas runtime. Karena type erasure, nama kelas yang dicetak adalah kelas raw, bukan tipe generic.
  2. cetakTipeRuntime(daftarInt): mencetak ArrayList, bukan List<Int>. Informasi Int hilang saat kompilasi. Hal yang sama terjadi pada daftarStr — ArrayList juga, meskipun type argument berbeda.
  3. daftarInt::class == daftarStr::class: hasilnya true karena keduanya menjadi kelas yang sama (java.util.Arrays$ArrayList) di runtime. Type erasure menghapus perbedaan.
  4. if (x is List<*>): pemeriksaan is List<*> valid karena hanya memeriksa kelas raw. Kita tidak bisa menulis is List<String> — kompilator menolak karena informasi String tidak tersedia di runtime.
  5. when (e) { is Int -> ... }: pemeriksaan tipe elemen individual masih bisa dilakukan. Hal ini karena kita memeriksa tipe runtime nilai individual, bukan type argument koleksi.
  6. fun prosesList(daftar: List<Int>): function menerima List<Int>. Saat runtime, parameter hanya List, tetapi kompilator tetap memverifikasi setiap akses elemen sebagai Int.
  7. for (n in daftar) total += n: kompilator menyisipkan cast implisit ke Int saat membaca elemen. Cast ini aman karena type safety sudah diverifikasi saat kompilasi.

Konsekuensi Type Erasure #

Type erasure membawa beberapa konsekuensi praktis. Pertama, kita tidak dapat memeriksa is List<String> di runtime. Kedua, kita tidak dapat membuat array generic seperti Array<T> tanpa type argument konkret — kompilator memerlukan Array<Int> atau Array<String>. Ketiga, refleksi tidak memberikan informasi type argument pada instance, hanya pada deklarasi.

Cara Mengatasi Type Erasure #

Salah satu cara mengatasi type erasure adalah dengan memakai reified type parameters — fitur Kotlin yang mempertahankan informasi tipe generic untuk function inline. Dengan reified, kita dapat melakukan if (item is T) di dalam function, karena kompilator menggabungkan function ke dalam pemanggil dan mengetahui tipe konkret pada lokasi pemanggilan.

Referensi Lanjutan #

Reified tidak mengatasi semua batasan type erasure — kita masih tidak dapat membuat Array<T> generic tanpa type argument konkret. Namun, reified cukup untuk banyak kasus umum seperti filter berdasarkan tipe atau pembuatan tag tipe. Pada chapter reified Type Parameters kita akan membedah fitur ini secara mendalam.