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>).
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:
| Operasi | Bisa? | Catatan |
|---|---|---|
x is List<*> | Ya | Hanya cek tipe raw |
x is List<String> | Tidak | Informasi String hilang |
x::class | Ya | Mengembalikan kelas raw (mis. ArrayList) |
elemen is String saat iterasi | Ya | Cek tipe elemen individual |
Membuat array Array<T> generic | Tidak | Harus reified atau Array<Any?> |
Demonstrasi Type Erasure #
Mari kita lihat bagaimana type erasure memengaruhi kode yang kita tulis:
// 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:
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: 6Mari kita telaah baris-baris kode di atas:
fun cetakTipeRuntime(item: Any): function menerimaAnylalu mencetak nama kelas runtime. Karena type erasure, nama kelas yang dicetak adalah kelas raw, bukan tipe generic.cetakTipeRuntime(daftarInt): mencetakArrayList, bukanList<Int>. InformasiInthilang saat kompilasi. Hal yang sama terjadi padadaftarStr—ArrayListjuga, meskipun type argument berbeda.daftarInt::class == daftarStr::class: hasilnyatruekarena keduanya menjadi kelas yang sama (java.util.Arrays$ArrayList) di runtime. Type erasure menghapus perbedaan.if (x is List<*>): pemeriksaanis List<*>valid karena hanya memeriksa kelas raw. Kita tidak bisa menulisis List<String>— kompilator menolak karena informasiStringtidak tersedia di runtime.when (e) { is Int -> ... }: pemeriksaan tipe elemen individual masih bisa dilakukan. Hal ini karena kita memeriksa tipe runtime nilai individual, bukan type argument koleksi.fun prosesList(daftar: List<Int>): function menerimaList<Int>. Saat runtime, parameter hanyaList, tetapi kompilator tetap memverifikasi setiap akses elemen sebagaiInt.for (n in daftar) total += n: kompilator menyisipkan cast implisit keIntsaat 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 type parameters
— penjelasan resmi type erasure pada JVM dan cara
reifiedmengatasinya
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.