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.classbiasa — subtype dengan body dan method.object— subtype singleton tanpa state.sealed classlain — 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.
// 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:
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 keluarMari kita bedah kode di atas:
sealed class Hasil { data class Berhasil(...) : Hasil(); ...; object SedangMemuat : Hasil() }: sealed class dengan empat subtype. Tiga subtype adalahdata classyang menyimpan state berbeda —Berhasilpunyadatadanjumlah,Gagalpunyakodedanpesan,Sebagianpunya dua list. Satu subtype adalahobject SedangMemuattanpa state.when (h) { is Hasil.Berhasil -> ...; Hasil.SedangMemuat -> "..." }:whenyang exhaustive. Cabangis Hasil.Berhasilmemakai smart cast — setelah cabang cocok,hotomatis diperlakukan sebagaiHasil.Berhasilsehingga propertydatadanjumlahdapat diakses. CabangHasil.SedangMemuattidak memakaiiskarena berupaobject— kita dapat membandingkan langsung dengan referensi singleton.sealed class Bangun { abstract fun luas(): Double; class Lingkaran(...) : Bangun() { override fun luas() = ... } }: sealed class dengan abstract member. Setiap subtype wajib mengoverrideluas(). MethodringkasanLuasmemakaiwhenuntuk memilih format output sesuai subtype.is Bangun.Lingkaran -> "Lingkaran, luas = ${b.luas()}": setelah smart cast, kita dapat memanggilb.luas()— method dari abstract classBangun. Tidak perlu memeriksa property spesifik subtype.sealed class StatusLogin { object Belum; object Berhasil; object Keluar; data class Gagal(val alasan: String) }: sealed class dengan tiga subtypeobject(tanpa state) dan satudata class(dengan state). Pola umum untuk merepresentasikan state mesin — sebagian state tidak punya data tambahan, sebagian lainnya punya detail.is StatusLogin.Gagal -> "Login gagal: ${s.alasan}": smart cast mengaktifkan akses ke propertyalasanyang hanya dimilikiStatusLogin.Gagal. Cabang lain (Belum,Berhasil,Keluar) membandingkan langsung dengan referensiobject.
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:
| Aspek | sealed class | abstract class | enum class |
|---|---|---|---|
| Subtype | Terbatas, di file yang sama | Tidak terbatas | Tetap, terdaftar di body |
| State per subtype | Berbeda-beda | Berbeda-beda | Sama (dari constructor enum) |
Exhaustive when | Ya | Tidak (perlu else) | Ya |
| Instance per subtype | Bebas | Bebas | Satu 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 #
- Sealed classes
— panduan resmi
sealed classuntuk hierarki tertutup
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.