Enum baik untuk merepresentasikan nilai diskret, namun terbatas — setiap konstanta hanya menyimpan nama, ordinal, dan property yang sama di semua konstanta. Ketika kita perlu merepresentasikan hierarki yang setiap subtype-nya dapat menyimpan state berbeda, enum tidak cukup. Kotlin menyediakan sealed class — class abstract yang subtype-nya terbatas dan harus berada di file yang sama. Kompilator mengetahui seluruh subtype tersebut, sehingga when pada sealed class bersifat exhaustive tanpa perlu cabang else. Pada chapter ini kita akan mendeklarasikan sealed class dengan beberapa subtype, memakai when yang exhaustive, dan membandingkan pola sealed class dengan alternatif lain.

Apa Itu Sealed Class #

sealed class adalah class abstract yang dideklarasikan dengan keyword sealed. Subclass langsung dari sealed class harus berada di file yang sama (atau package yang sama sejak Kotlin 1.5), sehingga kompilator mengetahui seluruh subtype yang ada di dunia. Karena daftar subtype lengkap dan terbatas, kompilator dapat memverifikasi bahwa when pada sealed class telah menangani semua kemungkinan — tanpa perlu cabang else sebagai jaring pengaman.

Subclass dari sealed class dapat berupa:

  • data class — subtype yang menyimpan state.
  • class biasa — subtype dengan body dan method.
  • object — subtype singleton tanpa state.
  • sealed class lain — subtype yang juga bersifat sealed (subhierarki).

Subclass dapat memiliki property dan jumlah constructor yang berbeda — tidak seperti enum yang seluruh konstantanya memiliki tanda tangan constructor yang sama.

Sealed Class untuk Hasil Operasi #

Pola paling umum sealed class adalah merepresentasikan hasil operasi yang dapat berupa sukses, gagal, atau kondisi antara. Contoh berikut mendefinisikan sealed class Hasil dengan empat subtype: Berhasil, Gagal, Sebagian, dan SedangMemuat. Function proses(h: Hasil) memakai when yang exhaustive — kompilator memverifikasi semua cabang telah ditangani.

kotlin
// sealed_classes.kt
// Mendemonstrasikan `sealed class` Kotlin — restricted hierarchy.

sealed class Hasil {
    // Subclass harus berada di file yang sama (atau package yang sama
    // sejak Kotlin 1.5).
    data class Berhasil(val data: String, val jumlah: Int) : Hasil()
    data class Gagal(val kode: Int, val pesan: String) : Hasil()
    data class Sebagian(val berhasil: List<String>, val gagal: List<String>) : Hasil()
    object SedangMemuat : Hasil()
}

// Sealed class dengan abstract member.
sealed class Bangun {
    abstract fun luas(): Double

    class Lingkaran(private val r: Double) : Bangun() {
        override fun luas(): Double = Math.PI * r * r
    }

    class Persegi(private val sisi: Double) : Bangun() {
        override fun luas(): Double = sisi * sisi
    }

    class Segitiga(private val alas: Double, private val tinggi: Double) : Bangun() {
        override fun luas(): Double = 0.5 * alas * tinggi
    }
}

// Sealed class tanpa data — murni tag state.
sealed class StatusLogin {
    object Belum : StatusLogin()
    object Berhasil : StatusLogin()
    object Keluar : StatusLogin()
    data class Gagal(val alasan: String) : StatusLogin()
}

fun proses(h: Hasil): String = when (h) {
    is Hasil.Berhasil -> "Berhasil: ${h.data} (${h.jumlah} item)"
    is Hasil.Gagal -> "Gagal [${h.kode}]: ${h.pesan}"
    is Hasil.Sebagian -> "Sebagian: ${h.berhasil.size} berhasil, ${h.gagal.size} gagal"
    Hasil.SedangMemuat -> "Sedang memuat..."
}

fun ringkasanLuas(b: Bangun): String = when (b) {
    is Bangun.Lingkaran -> "Lingkaran, luas = ${b.luas()}"
    is Bangun.Persegi -> "Persegi, luas = ${b.luas()}"
    is Bangun.Segitiga -> "Segitiga, luas = ${b.luas()}"
}

fun pesanStatus(s: StatusLogin): String = when (s) {
    StatusLogin.Belum -> "Pengguna belum login"
    StatusLogin.Berhasil -> "Pengguna berhasil login"
    StatusLogin.Keluar -> "Pengguna keluar"
    is StatusLogin.Gagal -> "Login gagal: ${s.alasan}"
}

fun main() {
    val daftarHasil: List<Hasil> = listOf(
        Hasil.Berhasil("Pengguna Budi", 1),
        Hasil.Gagal(404, "Tidak ditemukan"),
        Hasil.Sebagian(listOf("Budi", "Siti"), listOf("Dewi")),
        Hasil.SedangMemuat,
    )
    for (h in daftarHasil) {
        println(proses(h))
    }
    println()

    val bangunan: List<Bangun> = listOf(
        Bangun.Lingkaran(3.0),
        Bangun.Persegi(4.0),
        Bangun.Segitiga(6.0, 4.0),
    )
    for (b in bangunan) {
        println(ringkasanLuas(b))
    }
    println()

    val alurStatus: List<StatusLogin> = listOf(
        StatusLogin.Belum,
        StatusLogin.Berhasil,
        StatusLogin.Gagal("kata sandi salah"),
        StatusLogin.Keluar,
    )
    for (s in alurStatus) {
        println(pesanStatus(s))
    }
}

Output yang Dihasilkan:

text
Berhasil: Pengguna Budi (1 item)
Gagal [404]: Tidak ditemukan
Sebagian: 2 berhasil, 1 gagal
Sedang memuat...

Lingkaran, luas = 28.274333882308138
Persegi, luas = 16.0
Segitiga, luas = 12.0

Pengguna belum login
Pengguna berhasil login
Login gagal: kata sandi salah
Pengguna keluar

Mari kita bedah kode di atas:

  1. sealed class Hasil { data class Berhasil(...) : Hasil(); ...; object SedangMemuat : Hasil() }: sealed class dengan empat subtype. Tiga subtype adalah data class yang menyimpan state berbeda — Berhasil punya data dan jumlah, Gagal punya kode dan pesan, Sebagian punya dua list. Satu subtype adalah object SedangMemuat tanpa state.
  2. when (h) { is Hasil.Berhasil -> ...; Hasil.SedangMemuat -> "..." }: when yang exhaustive. Cabang is Hasil.Berhasil memakai smart cast — setelah cabang cocok, h otomatis diperlakukan sebagai Hasil.Berhasil sehingga property data dan jumlah dapat diakses. Cabang Hasil.SedangMemuat tidak memakai is karena berupa object — kita dapat membandingkan langsung dengan referensi singleton.
  3. sealed class Bangun { abstract fun luas(): Double; class Lingkaran(...) : Bangun() { override fun luas() = ... } }: sealed class dengan abstract member. Setiap subtype wajib mengoverride luas(). Method ringkasanLuas memakai when untuk memilih format output sesuai subtype.
  4. is Bangun.Lingkaran -> "Lingkaran, luas = ${b.luas()}": setelah smart cast, kita dapat memanggil b.luas() — method dari abstract class Bangun. Tidak perlu memeriksa property spesifik subtype.
  5. sealed class StatusLogin { object Belum; object Berhasil; object Keluar; data class Gagal(val alasan: String) }: sealed class dengan tiga subtype object (tanpa state) dan satu data class (dengan state). Pola umum untuk merepresentasikan state mesin — sebagian state tidak punya data tambahan, sebagian lainnya punya detail.
  6. is StatusLogin.Gagal -> "Login gagal: ${s.alasan}": smart cast mengaktifkan akses ke property alasan yang hanya dimiliki StatusLogin.Gagal. Cabang lain (Belum, Berhasil, Keluar) membandingkan langsung dengan referensi object.

Keuntungan Exhaustiveness #

Keuntungan utama sealed class adalah exhaustiveness check. Saat kita menambah subtype baru ke sealed class, semua when yang memproses sealed class tersebut akan ditandai sebagai tidak lengkap oleh kompilator. Sifat ini memaksa kita memperbarui setiap when yang ada — tidak ada satu pun yang tertinggal. Tanpa sealed class, kita harus menambah else sebagai jaring pengaman, namun else sering kali menyembunyikan bug karena tidak menangani subtype baru secara eksplisit.

Sealed Class vs Abstract Class vs Enum #

Tabel berikut merangkum perbedaan ketiganya:

Aspeksealed classabstract classenum class
SubtypeTerbatas, di file yang samaTidak terbatasTetap, terdaftar di body
State per subtypeBerbeda-bedaBerbeda-bedaSama (dari constructor enum)
Exhaustive whenYaTidak (perlu else)Ya
Instance per subtypeBebasBebasSatu per konstanta

Pilih sealed class ketika subtype terbatas dan masing-masing menyimpan state berbeda. Pilih enum class ketika semua konstanta memiliki struktur yang sama. Pilih abstract class ketika daftar subtype dapat bertambah dari file lain.

Referensi Lanjutan #

Ringkasan #

sealed class membatasi subtype pada daftar yang dideklarasikan di file yang sama, sehingga kompilator mengetahui seluruh subtype yang ada. when pada sealed class bersifat exhaustive — cabang else tidak diperlukan, dan kompilator memberi peringatan saat ada subtype yang belum ditangani. Sealed class cocok untuk merepresentasikan hasil operasi, state mesin, dan hierarki nilai yang terbatas. Pada chapter sealed interface berikutnya, kita akan melihat batasan serupa pada interface.