Kelas Abstrak

Kontrak parsial dan pertimbangan interface versus abstract class.

Daftar isi

Kelas abstrak (abstract class) adalah kelas yang tidak dapat diinstansiasi dan dirancang menjadi kerangka bagi turunannya, dengan sebagian anggota sudah berisi implementasi dan sebagian lagi dideklarasikan abstrak untuk diisi oleh kelas turunan. Bentuk ini melengkapi interface ketika kerangka yang dibagi memerlukan keadaan internal atau constructor. Topik ini membahas deklarasi kelas abstrak beserta anggota abstrak dan konkretnya, penyimpanan keadaan yang hanya dimungkinkan di sini, dan pertimbangan memilih interface atau kelas abstrak. Seluruh contoh dijalankan nyata dan keluarannya ditampilkan sebagaimana adanya.

Deklarasi kelas abstrak#

Kelas abstrak dideklarasikan dengan kata kunci abstract di depan class, dan instansnya tidak pernah dibuat langsung. Yang dibuat adalah instans kelas turunannya, sedangkan tipe kelas abstrak tetap dipakai sebagai tipe variabel maupun tipe elemen daftar. Pola ini menyeragamkan perlakuan terhadap objek-objek yang sejenis tanpa memaksakan satu bentuk perilaku yang sama persis.

Buat berkas main.kt berikut, lalu jalankan dengan dua perintah di bawahnya.

kotlin main.kt
abstract class KartuUjian(val nomor: Int) {
    private var nilai = 0

    fun simpanNilai(nilai: Int) {
        if (nilai in 0..100) this.nilai = nilai
    }

    fun nilaiTersimpan(): Int {
        return nilai
    }

    abstract fun jenis(): String

    abstract fun batasWaktu(): Int
}

class KartuPilihanGanda(nomor: Int) : KartuUjian(nomor) {
    override fun jenis(): String {
        return "pilihan ganda"
    }

    override fun batasWaktu(): Int {
        return 90
    }
}

class KartuEsai(nomor: Int) : KartuUjian(nomor) {
    override fun jenis(): String {
        return "esai"
    }

    override fun batasWaktu(): Int {
        return 120
    }
}

fun main() {
    val kartu: List<KartuUjian> = listOf(
        KartuPilihanGanda(1),
        KartuEsai(2)
    )
    for (k in kartu) {
        val skor = if (k.jenis() == "esai") 88 else 92
        k.simpanNilai(skor)
        println("Kartu ${k.nomor} (${k.jenis()}) bernilai ${k.nilaiTersimpan()}, waktu ${k.batasWaktu()} menit")
    }
}
bash
# compiler Kotlin dan pustaka standar yang dipakai jalur ini
export PATH=/home/kal/my-project/tools/kotlinc/bin:/home/kal/my-project/tools/jdk-merged/bin:$PATH
export STDLIB=/home/kal/my-project/tools/kotlinc/lib/kotlin-stdlib.jar
# kompilasi berkas main.kt menjadi out.jar, lalu jalankan programnya
kotlinc main.kt -d out.jar && java -cp "out.jar:$STDLIB" MainKt
text Keluaran
Kartu 1 (pilihan ganda) bernilai 92, waktu 90 menit
Kartu 2 (esai) bernilai 88, waktu 120 menit

Kelas KartuUjian tidak pernah diinstansiasi, sedangkan dua turunannya mengisi jenis dan batasWaktu masing-masing. Variabel kartu bertipe List<KartuUjian> menampung kedua turunan sekaligus, sehingga perulangan for memperlakukan keduanya dengan tipe yang sama. Fungsi simpanNilai dan nilaiTersimpan dipanggil tanpa peduli turunan sebenarnya, karena keduanya milik kelas abstrak dan sudah lengkap isinya.

Anggota abstrak dan konkret#

Anggota abstrak dideklarasikan dengan abstract dan tanpa tubuh, sebagaimana jenis dan batasWaktu pada KartuUjian. Setiap kelas turunan yang bukan abstrak wajib menimpanya dengan override, dan penimpaan itu menjadi satu-satunya jalan melengkapi kontraknya. Sebaliknya, anggota konkret ditulis lengkap dengan tubuhnya, sehingga seluruh turunan memakai logika yang sama tanpa menyalinnya.

Pembagian ini menyusun kontrak yang parsial. Kelas abstrak menjanjikan perilaku yang sudah jadi, yaitu penjagaan rentang nilai pada simpanNilai, sekaligus menuntut bagian yang berbeda pada tiap turunan, yaitu jenis kartu dan batas waktunya. Turunan baru seperti KartuLisan cukup mengisi dua anggota abstrak itu dan langsung memperoleh seluruh logika bersamanya.

Keadaan pada kelas abstrak#

Keadaan internal, yaitu properti berpenyimpanan yang menyertai setiap instans, hanya dapat hidup di kelas. Pada KartuUjian, properti nilai dinyatakan private dengan nilai awal 0, dan dua fungsi konkret menjadi satu-satunya jalan membaca atau mengisinya. Kelas abstrak karena itu mampu menegakkan aturan yang melibatkan data, misalnya rentang nilai 0 sampai 100 yang diperiksa sebelum penyimpanan.

Interface tidak menawarkan kemampuan yang sama, karena propertinya tidak boleh memiliki inisialisator maupun backing field. Kontrak pada interface mampu menjanjikan pembacaan data, tetapi tidak mampu menyimpan atau menjaganya. Apabila aturan penyimpanan menjadi inti kerangka yang dibagi, kelas abstrak adalah wadah yang tepat, dan kontrak berupa kemampuan tanpa keadaan cukup dinyatakan dengan interface.

Memilih interface atau kelas abstrak#

Keduanya dapat mendeklarasikan anggota abstrak dan memberi implementasi bawaan, sehingga perbedaannya terletak pada hal-hal berikut.

Aspekinterfacekelas abstrak
Banyaknya induk per kelasboleh banyak sekaligushanya satu
Properti berpenyimpanantidak bolehboleh
Constructor dengan parametertidak adaada
Implementasi bawaan fungsibolehboleh
Logika yang menyentuh data internaltidak mungkinbebas

Pilih interface ketika yang dibagi adalah kemampuan yang melekat pada kelas-kelas yang tidak sekeluarga, misalnya kemampuan menghitung tarif pada kendaraan yang berbeda induknya. Pilih kelas abstrak ketika yang dibagi adalah kerangka dengan data dan logika bersama, sebagaimana KartuUjian yang menyimpan nilai di dalam dirinya. Keduanya dapat dipadukan, yaitu kelas abstrak mengimplementasikan interface tertentu dan turunannya mewarisi keduanya sekaligus.

Kesalahan umum#

Kesalahan yang paling langsung adalah mencoba membuat instans kelas abstrak. Berkas berikut sengaja memuat kesalahan tersebut.

kotlin salahinstansiasi.kt
abstract class KartuUjian(val nomor: Int) {
    abstract fun jenis(): String
}

fun main() {
    val kartu = KartuUjian(3)
    println(kartu.jenis())
}
bash
# kompilasi berkas salahinstansiasi.kt untuk melihat pesan error dari compiler
kotlinc salahinstansiasi.kt -d salah.jar
text Keluaran
salahinstansiasi.kt:6:17: error: cannot create an instance of an abstract class.
    val kartu = KartuUjian(3)
                ^^^^^^^^^^^^^

Pesan cannot create an instance of an abstract class menegaskan sifat dasarnya. Perbaikannya adalah membentuk instans salah satu turunan konkret, atau mendefinisikan turunan baru apabila belum ada yang cocok. Bentuk lain yang sah adalah turunan abstrak yang melanjutkan pengisian ke tingkat berikutnya, dan pada rantai seperti itu instans baru muncul di ujung yang seluruh anggota abstraknya sudah terisi.

Ringkasan#

  • Kelas abstrak dideklarasikan dengan abstract class dan tidak dapat diinstansiasi, sehingga instans selalu berasal dari turunannya.
  • Anggota abstrak ditulis tanpa tubuh dan wajib ditimpa oleh turunan konkret, sedangkan anggota konkret dipakai bersama tanpa penyalinan.
  • Properti berpenyimpanan beserta penjagaannya hanya mungkin hidup di kelas abstrak, bukan di interface.
  • Interface dipilih untuk kemampuan lintas keluarga kelas, sedangkan kelas abstrak dipilih untuk kerangka yang membawa keadaan dan constructor.

Referensi#