Kode yang kita tulis diproses dalam dua fase berbeda: saat kompilasi dan saat program berjalan. Pada chapter ini kita akan membedah apa yang diputuskan kompilator Kotlin pada fase compile-time versus apa yang baru diketahui JVM pada saat runtime. Memahami pembagian ini penting karena sebagian galat ditangkap kompilator (type mismatch, ambiguitas overload), sementara sebagian lain baru muncul saat program berjalan (dynamic dispatch, reflection).
Apa yang Diputus Compile-Time #
Kompilator Kotlin memproses kode sumber menjadi bytecode JVM dan menetapkan banyak hal yang tidak akan berubah saat runtime:
- Tipe setiap variabel dan expression —
val x: Int = 5menetapkan tipeIntpada bytecode; runtime tidak menyimpan info ini sebagai metadata variabel. const valdi-inline langsung — nilai konstanta compile-time disematkan langsung ke bytecode pada setiap titik pemakaian.- Type erasure pada generic —
List<String>danList<Int>menjadiListmentah saat runtime; info tipe parameter hilang. - Overload resolution — saat kita memanggil
println(42), kompilator memilih versiprintln(Int)di antara banyak overload yang ada. - Null-safety check — kompilator menolak
val x: String = nullsaat kompilasi, sebelum kode dijalankan.
Apa yang Baru Diketahui Runtime #
Sebaliknya, sebagian besar nilai dan keputusan eksekusi baru diketahui saat program berjalan:
- Nilai variabel —
val x = readLine()hanya diketahui setelah user mengetik input. - Dynamic dispatch — pemanggilan method pada objek yang dideklarasikan sebagai
opendipilih berdasarkan tipe aktual objek saat runtime. - Reflection —
obj::class.simpleNamemembaca nama class saat runtime melalui refleksi JVM. - Exception —
NumberFormatExceptionbaru dilempar saat parsing input yang tidak valid dijalankan.
Contoh yang Memperlihatkan Kedua Fase #
Berikut program yang memadukan keputusan compile-time dan runtime:
// FASE KOMPILASI:
// - Tipe variabel ditentukan: x:Int, daftar:List<String>
// - const val di-inline langsung ke bytecode (tidak ada field saat runtime)
// - Generic dihapus: List<String> menjadi List raw saat runtime (type erasure)
// - Overload resolution: println(Int) vs println(String) dipilih saat kompilasi
const val BATAS_ATAS: Int = 100
open class Hewan {
open fun suara(): String = "suara generik"
}
class Kucing : Hewan() {
override fun suara(): String = "meong"
}
class Anjing : Hewan() {
override fun suara(): String = "guk guk"
}
fun main() {
// Tipe 'x' ditentukan saat kompilasi sebagai Int
val x = 50
println("Nilai x = $x, tipe compile-time: Int")
// const val di-inline; saat runtime tidak ada lookup field BATAS_ATAS
if (x > BATAS_ATAS) {
println("x melebihi batas")
} else {
println("x dalam batas wajar: $BATAS_ATAS")
}
// Dynamic dispatch saat runtime: pemanggilan method dipilih berdasar tipe aktual objek
val daftar: List<Hewan> = listOf(Kucing(), Anjing(), Hewan())
for (hewan in daftar) {
println("Suara: ${hewan.suara()}")
}
// Type erasure: List<String> dan List<Int> menjadi List yang sama saat runtime
val daftarString: List<String> = listOf("Budi", "Siti")
val daftarInt: List<Int> = listOf(1, 2, 3)
println("Class daftarString: ${daftarString::class.simpleName}")
println("Class daftarInt: ${daftarInt::class.simpleName}")
}Output yang Dihasilkan:
Nilai x = 50, tipe compile-time: Int
x dalam batas wajar: 100
Suara: meong
Suara: guk guk
Suara: suara generik
Class daftarString: ArrayList
Class daftarInt: ArrayListMari kita bedah keputusan pada setiap fase:
- Baris 1-5 (komentar di atas
const val) — kompilator memproses komentar sebagai dokumentasi; komentar tidak memengaruhi bytecode. - Baris 7 (
const val BATAS_ATAS: Int = 100) — compile-time: nilai100disematkan langsung ke setiap bytecode yang memakaiBATAS_ATAS. Saat runtime, tidak ada fieldBATAS_ATASyang dapat di-query via reflection. - Baris 9-21 (deklarasi class
Hewan,Kucing,Anjing) — kompilator memverifikasi bahwaKucingdanAnjingsah mewarisiHewan(methodsuara()di-overridedengan tanda tangan yang sama). - Baris 25 (
val x = 50) — compile-time: tipexdisimpulkan sebagaiIntdari literal50. Saat runtime, bytecode hanya menyimpan referensi ke objekInteger. - Baris 28-31 (
if (x > BATAS_ATAS)) —BATAS_ATASsudah di-inline menjadi literal100, sehingga bytecode membandingkan50 > 100. Cabangelsedipilih saat runtime. - Baris 34-36 (
for (hewan in daftar)) — runtime: tipe statishewanadalahHewan, tetapi methodsuara()yang dipanggil bergantung pada tipe aktual objek (Kucing,Anjing, atauHewan). Inilah dynamic dispatch. - Baris 39-41 (
daftarString::class.simpleName) — runtime: refleksi membaca class aktual dari objek. Karena type erasure, kedua daftar menunjukkanArrayListmeskipun tipe compile-time berbeda (List<String>vsList<Int>).
const val vs val Biasa #
Perbedaan penting antara const val dan val biasa terletak pada kapan nilai ditentukan:
const val MAX_LAYANAN = 5 // di-inline ke bytecode
val VERSI: String = "1.0.0" // disimpan sebagai field, dibaca saat runtimeMAX_LAYANANdapat dipakai pada anotasi (yang memerlukan konstanta compile-time).VERSItidak bisa dipakai pada anotasi, meskipun nilainya tidak pernah berubah.- Refleksi
::MAX_LAYANANtidak mengembalikan apa pun — konstantaconst valtidak memiliki field runtime.
Type Erasure dan Konsekuensinya #
Generic di Kotlin (seperti Java) mengalami type erasure — tipe parameter hilang saat runtime:
fun cetak(daftar: List<String>) = println(daftar)
fun cetak(daftar: List<Int>) = println(daftar) // ERROR: signature sama setelah erasureKedua overload di atas tidak dapat hidup berdampingan karena setelah type erasure, keduanya memiliki signature cetak(List). Untuk mengatasinya, Kotlin menyediakan reified type parameter pada inline function:
inline fun <reified T> tipeSaatRuntime(value: T) {
println("Tipe T: ${T::class.simpleName}")
}Keyword reified membuat kompilator menyalin function ke setiap pemanggilan dengan tipe konkret, sehingga info tipe tidak hilang. Fitur ini hanya tersedia pada inline function.
Dynamic Dispatch pada Method open #
Method open (yang dapat di-override) dipanggil via dynamic dispatch: JVM menelusuri tabel virtual method saat runtime untuk menemukan implementasi yang sesuai dengan tipe aktual objek.
val hewan: Hewan = Kucing()
println(hewan.suara()) // mencetak "meong", bukan "suara generik"Tipe statis hewan adalah Hewan, tetapi JVM melihat tipe aktual objek Kucing dan memanggil Kucing.suara(). Inilah inti polimorfisme pada pemrograman berorientasi object.
Referensi Lanjutan #
- Kotlin basic syntax — ringkasan sintaks Kotlin yang relevan untuk membedakan tahap compile-time dan runtime
- Kotlin command-line compiler
— rujukan kompilator CLI untuk memahami tahap kompilasi
.ktke bytecode JVM
Tabel Ringkasan #
| Keputusan | Fase | Cara Memverifikasi |
|---|---|---|
Tipe variabel Int, String | Compile-time | Cek bytecode: tipe tidak ada sebagai metadata |
const val inlined | Compile-time | Refleksi tidak menemukan field |
| Overload resolution | Compile-time | Lihat bytecode pemanggilan |
Generic List<String> erasure | Compile-time | list::class.simpleName tetap ArrayList |
| Null-safety | Compile-time | null ditolak oleh kompilator |
Dynamic dispatch method open | Runtime | Output bergantung tipe aktual objek |
Reflection ::class.simpleName | Runtime | Membaca metadata class saat runtime |
Exception NumberFormatException | Runtime | Dilempar saat parsing gagal |
Pemisahan compile-time dan runtime adalah fondasi pemahaman galat di Kotlin. Galat yang ditangkap kompilator biasanya berupa type mismatch dan resolusi overload, sementara galat runtime mencakup null pointer, parsing gagal, dan dynamic dispatch tak terduga. Kita siap membedah cara membaca pesan kesalahan dari kedua fase tersebut — pembahasan lengkap menanti pada chapter Membaca Pesan Error.