Kotlin menetapkan bahwa semua class dan method bersifat final secara default — kebalikan dari Java yang membuat semuanya open (atau virtual) kecuali yang ditandai final. Keputusan desain ini bukan kebetulan; Kotlin mendorong komposisi daripada inheritance untuk sebagian besar kasus. Untuk membuka class atau method agar dapat diwariskan, kita perlu menandainya secara eksplisit dengan keyword open. Pada chapter ini kita akan menelusuri mengapa Kotlin memilih default final, cara membuka class dan method, serta penggunaan final override untuk menghentikan rantai override.
Mengapa Default final #
Inheritance yang tidak direncanakan sering menimbulkan bug halus. Saat sebuah superclass mengubah implementasi internal method, subclass yang meng-override method tersebut dapat rusak tanpa peringatan. Hal ini terutama berbahaya pada library yang dirilis ke publik — pengguna library mungkin mewariskan class yang sebenarnya tidak dirancang untuk diwariskan, lalu terkunci pada implementasi internal yang sewaktu-waktu berubah.
Dengan default final, Kotlin memaksa penulis class untuk secara sadar menyatakan: “class ini sengaja dirancang untuk dapat diwariskan”. Penandaan eksplisit ini menjadi dokumentasi bahwa superclass bersedia menjaga kontrak perilakunya untuk subclass. Class yang tidak ditandai open dianggap detail implementasi yang boleh berubah tanpa pemberitahuan.
Membuka Class dan Method #
Keyword open ditempatkan sebelum keyword class untuk membuka class, dan sebelum keyword fun untuk membuka method. Sebuah class harus open agar dapat diwariskan; method harus open agar dapat di-override. Dua keputusan ini bersifat terpisah — sebuah class open tetap dapat memiliki method final yang tidak dapat di-override.
open class Bentuk(val nama: String) {
fun tipe(): String = "Bentuk" // final, tidak bisa di-override
open fun keliling(): Int = 0 // open, bisa di-override
}
class Kotak(nama: String, val sisi: Int) : Bentuk(nama) {
override fun keliling(): Int = 4 * sisi
}Method tipe() bersifat final — subclass Kotak tidak dapat menggantikannya. Method keliling() bersifat open — subclass wajib menandai override dengan keyword override. Konfigurasi ini memberi penulis class kontrol granular atas apa yang dapat disesuaikan oleh subclass.
Hierarki Tiga Tingkat dan final override #
Inheritance dapat berlapis lebih dari satu tingkat. Pada hierarki Bentuk → Kotak → KotakWarna, subclass KotakWarna secara default masih dapat meng-override method yang sudah di-override oleh Kotak. Untuk menghentikan rantai override pada tingkat tertentu, gunakan final override — kombinasi yang menyatakan method ini boleh di-override sampai tingkat ini saja.
open class Bentuk {
open fun keliling(): Int = 0
}
open class Kotak(val sisi: Int) : Bentuk() {
final override fun keliling(): Int = 4 * sisi
}
// class KotakWarna(sisi: Int) : Kotak(sisi) {
// override fun keliling(): Int = ... // galat: keliling final di Kotak
// }Pada contoh di atas, final override di class Kotak membuat keliling() tidak dapat di-override lagi oleh subclass turunan KotakWarna. Mekanisme ini berguna saat subclass ingin mempertahankan implementasi tertentu sebagai kontrak yang tidak boleh diubah lebih lanjut.
Contoh Lengkap dengan Berbagai Kombinasi #
Contoh berikut menyatukan empat kombinasi: class final default, class open, method final default, dan method open. Class Kotak meng-override dua method — satu dengan final override (menghentikan rantai), satu dengan override biasa (masih bisa di-override turunan).
// open_classes.kt
// Mendemonstrasikan class final default, open class, open fun, dan final override.
// Class TANPA open: bersifat final, tidak dapat diwariskan.
class UtilDasar {
fun sapa(): String = "Halo dari UtilDasar"
}
// Class DENGAN open: dapat diwariskan.
open class Bentuk(val nama: String) {
// Method TANPA open: bersifat final, tidak dapat di-override.
fun tipe(): String = "Bentuk dasar"
// Method DENGAN open: dapat di-override.
open fun keliling(): Int = 0
// open fun lain: dapat di-override.
open fun deskripsi(): String = "Bentuk $nama"
}
class Kotak(nama: String, val sisi: Int) : Bentuk(nama) {
// Override final (default): subclass turunan tidak boleh override lagi.
final override fun keliling(): Int = 4 * sisi
// Override tanpa final: masih bisa di-override oleh subclass turunan.
override fun deskripsi(): String = "Kotak $nama dengan sisi $sisi"
}
fun main() {
val util = UtilDasar()
println(util.sapa())
val k = Kotak("A", 5)
println(k.tipe())
println("Keliling: ${k.keliling()}")
println(k.deskripsi())
// Upcasting: variabel Bentuk menampung Kotak.
val b: Bentuk = k
println("Dari referensi Bentuk: ${b.deskripsi()}")
}Output yang Dihasilkan:
Halo dari UtilDasar
Bentuk dasar
Keliling: 20
Kotak A dengan sisi 5
Dari referensi Bentuk: Kotak A dengan sisi 5Mari kita amati peran setiap penanda:
class UtilDasartanpaopen: class bersifat final. Kodeclass X : UtilDasar()akan ditolak kompilator.open class Bentuk(val nama: String): class dibuka untuk inheritance. SubclassKotaksah mewarisi class ini.fun tipe(): Stringtanpaopen: method bersifat final. Subclass tidak dapat meng-override meskipun class-nyaopen.open fun keliling(): Int: method dibuka untuk override. Subclass dapat mengganti denganoverride.final override fun keliling(): Int = 4 * sisi: subclassKotakmeng-overridekelilingdan menandainyafinal. Bila ada subclass turunan dariKotak, subclass tersebut tidak dapat meng-overridekelilinglagi.override fun deskripsi(): String: subclass meng-overridedeskripsitanpafinal. Subclass turunan masih dapat meng-overridedeskripsilagi.val b: Bentuk = k: upcasting dariKotakkeBentuk. Variabel bertipeBentuktetapi memanggil methoddeskripsi()akan memilih implementasiKotakkarena method-nyaopendan di-override. Inilah polymorphism yang aktif.
Kapan Menggunakan final override #
final override paling berguna dalam dua skenario:
- Subclass yang dirancang sebagai akhir hierarki: ketika sebuah class mengasumsikan tidak ada lagi subclass yang sah,
final overridemencegah subclass tak terduga mengganti implementasi kritis. - Invarian kritis yang tidak boleh diubah: method yang menjaga invarian penting (mis. perhitungan keamanan, urutan langkah, hasil caching) sebaiknya
final overrideagar subclass tidak sengaja melemahkan.
Referensi Lanjutan #
- Inheritance — penjelasan resmi mengapa class Kotlin bersifat final secara default
Ringkasan #
Kotlin memilih default final untuk mendorong komposisi daripada inheritance tanpa rencana. Keyword open membuka class dan method untuk inheritance/override secara eksplisit, sedangkan final override menghentikan rantai override pada tingkat tertentu. Pemilihan yang teliti atas open, final, dan final override menjaga kontrak class tetap jelas bagi penulis maupun pengguna. Pada chapter Overriding Properties dan Methods berikutnya, kita akan melihat bahwa property juga dapat di-override, dengan aturan khusus terkait mutabilitas val dan var.