Enum

Enum #

Enum adalah salah satu alat paling underrated dalam pemrograman — terlalu sering digantikan dengan String atau konstanta integer yang membuat kode rentan terhadap typo, sulit di-refactor, dan tidak terbaca. Kotlin mengangkat enum jauh melampaui Java: enum Kotlin bisa memiliki property, method, implementasi interface, dan bahkan implementasi yang berbeda per entry. Ini menjadikan enum Kotlin bukan sekadar daftar konstanta, melainkan tipe data yang kaya dan ekspresif. Dikombinasikan dengan when expression yang exhaustive, enum menjadi fondasi yang kuat untuk modeling state, konfigurasi, dan logika bisnis yang terstruktur. Artikel ini membahas seluruh kapabilitas enum Kotlin, perbedaannya dengan sealed class, dan pola idiomatik yang membuat kode lebih aman dan lebih mudah dipahami.

Deklarasi Enum Dasar #

// Deklarasi paling sederhana
enum class Arah {
    UTARA, SELATAN, TIMUR, BARAT
}

enum class Status {
    AKTIF, NONAKTIF, PENDING, DIHAPUS
}

enum class Prioritas {
    RENDAH, SEDANG, TINGGI, KRITIS
}

// Penggunaan
val arah = Arah.UTARA
val status = Status.AKTIF

// Perbandingan
println(arah == Arah.UTARA)    // true
println(arah == Arah.SELATAN)  // false

// Enum adalah tipe — tidak bisa assign nilai sembarangan
// val salah: Arah = "UTARA"   // ERROR: Type mismatch

Property Bawaan #

Setiap enum entry secara otomatis memiliki dua property bawaan: name (nama sebagai String) dan ordinal (posisi berbasis nol).

enum class Planet {
    MERKURIUS, VENUS, BUMI, MARS, JUPITER, SATURNUS, URANUS, NEPTUNUS
}

val planet = Planet.BUMI

println(planet.name)     // "BUMI"
println(planet.ordinal)  // 2 (indeks berbasis 0)

// Iterasi semua entry dengan entries (Kotlin 1.9+)
Planet.entries.forEach { println("${it.ordinal}: ${it.name}") }
// 0: MERKURIUS
// 1: VENUS
// 2: BUMI
// ...

// values() — cara lama, masih bisa dipakai tapi entries lebih disukai
val semuaPlanet: Array<Planet> = Planet.values()
entries diperkenalkan di Kotlin 1.9 sebagai pengganti values(). Perbedaan utama: entries mengembalikan List<T> yang immutable dan lebih efisien, sedangkan values() mengalokasikan array baru setiap dipanggil. Untuk kode baru, selalu gunakan entries.

Enum dengan Property #

Ini adalah salah satu kelebihan terbesar enum Kotlin dibanding Java — setiap entry bisa memiliki data yang terkait.

// Enum dengan property — deklarasikan di constructor
enum class HttpStatus(val kode: Int, val pesan: String) {
    OK(200, "OK"),
    CREATED(201, "Created"),
    BAD_REQUEST(400, "Bad Request"),
    UNAUTHORIZED(401, "Unauthorized"),
    FORBIDDEN(403, "Forbidden"),
    NOT_FOUND(404, "Not Found"),
    INTERNAL_SERVER_ERROR(500, "Internal Server Error")
}

// Akses property
val status = HttpStatus.NOT_FOUND
println(status.kode)    // 404
println(status.pesan)   // "Not Found"
println("${status.kode} ${status.pesan}")  // "404 Not Found"

// Contoh yang lebih kaya — satuan ukuran dengan faktor konversi
enum class SatuanBerat(val kgFactor: Double, val simbol: String) {
    GRAM(0.001, "g"),
    KILOGRAM(1.0, "kg"),
    TON(1000.0, "t"),
    POUND(0.453592, "lb"),
    OUNCE(0.0283495, "oz");

    fun keKilogram(nilai: Double): Double = nilai * kgFactor
    fun dariKilogram(kg: Double): Double = kg / kgFactor
}

fun konversi(nilai: Double, dari: SatuanBerat, ke: SatuanBerat): Double {
    val kg = dari.keKilogram(nilai)
    return ke.dariKilogram(kg)
}

println(konversi(1.0, SatuanBerat.KILOGRAM, SatuanBerat.GRAM))   // 1000.0
println(konversi(1.0, SatuanBerat.POUND, SatuanBerat.KILOGRAM))  // 0.453592
println(konversi(500.0, SatuanBerat.GRAM, SatuanBerat.OUNCE))    // 17.637...

Enum dengan Property Mutable #

Property enum bisa var — tapi ini jarang dipakai dan perlu hati-hati karena enum biasanya diharapkan immutable.

// Jarang direkomendasikan, tapi valid
enum class Konfigurasi(var nilai: String) {
    HOST("localhost"),
    PORT("8080"),
    DEBUG("false")
}

// Bisa diubah — tapi perubahan bersifat global!
Konfigurasi.HOST.nilai = "api.example.com"
println(Konfigurasi.HOST.nilai)   // "api.example.com"

// ANTI-PATTERN: enum mutable untuk state yang berubah-ubah
// Gunakan data class atau class biasa untuk state yang dinamis

Enum dengan Method #

Enum bisa memiliki method biasa dan method abstrak yang diimplementasikan berbeda di setiap entry.

Method Biasa #

enum class Musim(val bulan: IntRange) {
    SEMI(3..5),
    PANAS(6..8),
    GUGUR(9..11),
    DINGIN(12..2);  // 12, 1, 2

    fun apakahBulanIni(bulan: Int): Boolean = bulan in this.bulan

    fun berikutnya(): Musim {
        val semua = entries
        return semua[(ordinal + 1) % semua.size]
    }

    fun sebelumnya(): Musim {
        val semua = entries
        return semua[(ordinal - 1 + semua.size) % semua.size]
    }
}

println(Musim.SEMI.berikutnya())    // PANAS
println(Musim.DINGIN.berikutnya())  // SEMI (wrap around)
println(Musim.SEMI.apakahBulanIni(4))  // true

Method Abstrak — Implementasi Berbeda per Entry #

Ini adalah fitur paling powerful dari enum Kotlin: setiap entry bisa memiliki implementasi berbeda dari method yang sama.

// Enum dengan method abstrak — setiap entry mengimplementasikannya
enum class Operasi(val simbol: Char) {
    TAMBAH('+') {
        override fun hitung(a: Double, b: Double): Double = a + b
    },
    KURANG('-') {
        override fun hitung(a: Double, b: Double): Double = a - b
    },
    KALI('*') {
        override fun hitung(a: Double, b: Double): Double = a * b
    },
    BAGI('/') {
        override fun hitung(a: Double, b: Double): Double {
            require(b != 0.0) { "Tidak bisa membagi dengan nol" }
            return a / b
        }
    };

    abstract fun hitung(a: Double, b: Double): Double

    override fun toString() = simbol.toString()
}

// Penggunaan
val hasil = Operasi.KALI.hitung(6.0, 7.0)   // 42.0
println("6 ${Operasi.KALI} 7 = $hasil")      // "6 * 7 = 42.0"

// Kalkulator sederhana
fun kalkulasi(a: Double, simbol: Char, b: Double): Double {
    val op = Operasi.entries.find { it.simbol == simbol }
        ?: throw IllegalArgumentException("Operator tidak dikenal: $simbol")
    return op.hitung(a, b)
}

kalkulasi(10.0, '+', 5.0)   // 15.0
kalkulasi(10.0, '/', 3.0)   // 3.333...

// Contoh lain: strategi diskon berbeda per tipe member
enum class TipeMember {
    REGULER {
        override fun hitungDiskon(harga: Double) = 0.0
        override fun batasBeliGratis() = Int.MAX_VALUE
    },
    SILVER {
        override fun hitungDiskon(harga: Double) = harga * 0.05
        override fun batasBeliGratis() = 500_000
    },
    GOLD {
        override fun hitungDiskon(harga: Double) = harga * 0.10
        override fun batasBeliGratis() = 200_000
    },
    PLATINUM {
        override fun hitungDiskon(harga: Double) = harga * 0.20
        override fun batasBeliGratis() = 0
    };

    abstract fun hitungDiskon(harga: Double): Double
    abstract fun batasBeliGratis(): Int

    fun hargaAkhir(harga: Double): Double = harga - hitungDiskon(harga)
    fun gratisOngkir(totalBelanja: Int): Boolean = totalBelanja >= batasBeliGratis()
}

val member = TipeMember.GOLD
println(member.hargaAkhir(100_000.0))          // 90000.0
println(member.gratisOngkir(250_000))           // true

Enum dan Interface #

Enum bisa mengimplementasikan interface — ini adalah cara untuk memastikan semua entry memiliki kontrak yang sama.

interface Deskripsi {
    fun deskripsi(): String
}

interface Dapat dihitung {
    fun nilai(): Int
}

enum class Kartu(val simbol: String) : Deskripsi {
    AS("A") {
        override fun deskripsi() = "As — bisa bernilai 1 atau 11"
    },
    DUA("2") {
        override fun deskripsi() = "Dua — bernilai 2"
    },
    RAJA("K") {
        override fun deskripsi() = "Raja — bernilai 10"
    },
    RATU("Q") {
        override fun deskripsi() = "Ratu — bernilai 10"
    },
    JACK("J") {
        override fun deskripsi() = "Jack — bernilai 10"
    };
}

// Interface dengan implementasi default di enum body
interface Formatabel {
    fun format(): String
}

enum class StatusPesanan(val kode: String, val label: String) : Formatabel {
    MENUNGGU_PEMBAYARAN("WAIT_PAY", "Menunggu Pembayaran"),
    DIBAYAR("PAID", "Sudah Dibayar"),
    DIPROSES("PROCESSING", "Sedang Diproses"),
    DIKIRIM("SHIPPED", "Dalam Pengiriman"),
    DITERIMA("DELIVERED", "Diterima"),
    DIBATALKAN("CANCELLED", "Dibatalkan");

    override fun format(): String = "[$kode] $label"

    fun isAktif(): Boolean = this !in listOf(DITERIMA, DIBATALKAN)

    fun bisaDibatalkan(): Boolean = this in listOf(MENUNGGU_PEMBAYARAN, DIBAYAR)

    fun transisiBerikutnya(): List<StatusPesanan> = when (this) {
        MENUNGGU_PEMBAYARAN -> listOf(DIBAYAR, DIBATALKAN)
        DIBAYAR -> listOf(DIPROSES, DIBATALKAN)
        DIPROSES -> listOf(DIKIRIM)
        DIKIRIM -> listOf(DITERIMA)
        DITERIMA, DIBATALKAN -> emptyList()
    }
}

val pesanan = StatusPesanan.DIBAYAR
println(pesanan.format())                // "[PAID] Sudah Dibayar"
println(pesanan.isAktif())              // true
println(pesanan.bisaDibatalkan())       // true
println(pesanan.transisiBerikutnya())   // [DIPROSES, DIBATALKAN]

Enum di when Expression #

when dengan enum adalah exhaustive secara otomatis — compiler memastikan semua entry ditangani jika digunakan sebagai expression.

enum class Cuaca { CERAH, BERAWAN, HUJAN, BADAI }

// when sebagai expression — harus exhaustive (semua case ditangani)
fun rekomendasiAktivitas(cuaca: Cuaca): String = when (cuaca) {
    Cuaca.CERAH -> "Sempurna untuk outdoor!"
    Cuaca.BERAWAN -> "Jalan-jalan santai"
    Cuaca.HUJAN -> "Baca buku di rumah"
    Cuaca.BADAI -> "Tetap di dalam ruangan"
    // Tidak perlu else — compiler tahu semua case sudah ditangani
}

// Jika ada entry baru yang ditambahkan ke enum tapi when tidak diupdate:
// COMPILER ERROR — ini adalah keunggulan besar vs String atau Int

// Grouping beberapa case
fun butuhPayung(cuaca: Cuaca): Boolean = when (cuaca) {
    Cuaca.HUJAN, Cuaca.BADAI -> true
    Cuaca.CERAH, Cuaca.BERAWAN -> false
}

// when sebagai statement — else opsional tapi direkomendasikan
fun log(cuaca: Cuaca) {
    when (cuaca) {
        Cuaca.BADAI -> println("PERINGATAN: Badai terdeteksi!")
        else -> println("Cuaca: ${cuaca.name}")
    }
}

// Memanfaatkan property enum di when
enum class Level(val minSkor: Int, val warna: String) {
    PEMULA(0, "abu-abu"),
    MENENGAH(50, "hijau"),
    MAHIR(80, "biru"),
    EXPERT(95, "emas")
}

fun deskripsiLevel(level: Level): String = when (level) {
    Level.PEMULA -> "Baru memulai perjalanan"
    Level.MENENGAH -> "Progres yang baik!"
    Level.MAHIR -> "Hampir di puncak"
    Level.EXPERT -> "Master sejati!"
}

// Mendapatkan level dari skor
fun levelDariSkor(skor: Int): Level =
    Level.entries.lastOrNull { skor >= it.minSkor } ?: Level.PEMULA

Operasi Pencarian pada Enum #

enum class Mata uang(val kode: String, val simbol: String) {
    IDR("IDR", "Rp"),
    USD("USD", "$"),
    EUR("EUR", "€"),
    SGD("SGD", "S$"),
    MYR("MYR", "RM")
}

// valueOf — cari berdasarkan nama (case-sensitive, throw exception jika tidak ada)
val idr = MataUang.valueOf("IDR")   // MataUang.IDR
// MataUang.valueOf("idr")           // IllegalArgumentException!

// Cara aman dengan runCatching
fun String.toMataUangOrNull(): MataUang? =
    runCatching { MataUang.valueOf(uppercase()) }.getOrNull()

"usd".toMataUangOrNull()    // MataUang.USD
"xyz".toMataUangOrNull()    // null

// Pencarian berdasarkan property lain
fun cariBerdasarkanKode(kode: String): MataUang? =
    MataUang.entries.find { it.kode == kode }

fun cariBerdasarkanSimbol(simbol: String): MataUang? =
    MataUang.entries.find { it.simbol == simbol }

cariBerdasarkanKode("EUR")   // MataUang.EUR
cariBerdasarkanSimbol("S$")  // MataUang.SGD

// Dari ordinal
fun fromOrdinal(ordinal: Int): MataUang? =
    MataUang.entries.getOrNull(ordinal)

fromOrdinal(0)   // MataUang.IDR
fromOrdinal(99)  // null

Enum vs Sealed Class #

Enum dan sealed class keduanya merepresentasikan himpunan tipe tertutup, tapi dengan trade-off berbeda.

flowchart TD
    A{Semua instance\npunya shape yang sama?} -- Ya --> B["Enum\nSatu kelas, banyak instance\nSetiap entry: tipe dan data sama"]
    A -- Tidak --> C["Sealed Class\nBanyak subclass berbeda\nSetiap subclass: data berbeda"]
    B --> D["enum class Status { AKTIF, NONAKTIF }\nSemua punya name dan ordinal"]
    C --> E["sealed class Hasil\ndata class Sukses(val data: T)\ndata class Gagal(val pesan: String)\nobject Memuat"]
// Kapan Enum lebih tepat: semua entry punya struktur yang sama
enum class StatusKoneksi(val label: String) {
    TERHUBUNG("Terhubung"),
    TERPUTUS("Terputus"),
    MENGHUBUNGKAN("Menghubungkan..."),
    ERROR("Error")
}

// Kapan Sealed Class lebih tepat: setiap kasus punya data berbeda
sealed class HasilKoneksi {
    object Terhubung : HasilKoneksi()
    data class Gagal(val pesan: String, val kode: Int) : HasilKoneksi()
    data class Timeout(val detikMenunggu: Int) : HasilKoneksi()
    object SedangMenghubungkan : HasilKoneksi()
}

// Enum: tidak bisa menyimpan data berbeda per entry (kecuali di body)
// ANTI-PATTERN: mencoba menyimpan data berbeda di enum biasa
enum class EventBuruk {
    KLIK,      // tidak butuh data
    KETIK,     // butuh karakter yang diketik — tidak bisa!
    SCROLL     // butuh delta — tidak bisa!
}

// BENAR: sealed class untuk event dengan data berbeda
sealed class Event {
    object Klik : Event()
    data class Ketik(val karakter: Char) : Event()
    data class Scroll(val deltaY: Float) : Event()
}
EnumSealed Class
Struktur entrySemua samaBisa berbeda
entries / iterasi✓ Bawaan✗ Tidak ada
ordinal dan name✓ Bawaan✗ Tidak ada
valueOf✓ Bawaan✗ Tidak ada
Data berbeda per case✗ Terbatas✓ Bebas
Instance berbeda✗ Singleton✓ Bisa banyak
Cocok untukKonstanta, state, kategoriResult, event, ADT

Pola Idiomatik dengan Enum #

Enum sebagai State Machine #

enum class StatusPintu {
    TERTUTUP, TERBUKA, TERKUNCI, RUSAK;

    fun bisaDibuka(): Boolean = this == TERTUTUP
    fun bisaDitutup(): Boolean = this == TERBUKA
    fun bisaDikunci(): Boolean = this == TERTUTUP
    fun bisaDiperbaiki(): Boolean = this == RUSAK

    fun buka(): StatusPintu {
        require(bisaDibuka()) { "Pintu tidak bisa dibuka dari status $name" }
        return TERBUKA
    }

    fun tutup(): StatusPintu {
        require(bisaDitutup()) { "Pintu tidak bisa ditutup dari status $name" }
        return TERTUTUP
    }

    fun kunci(): StatusPintu {
        require(bisaDikunci()) { "Pintu tidak bisa dikunci dari status $name" }
        return TERKUNCI
    }
}

var pintu = StatusPintu.TERTUTUP
pintu = pintu.buka()     // TERBUKA
pintu = pintu.tutup()    // TERTUTUP
pintu = pintu.kunci()    // TERKUNCI
// pintu = pintu.buka()  // IllegalArgumentException: Pintu tidak bisa dibuka dari status TERKUNCI

Enum untuk Konfigurasi #

enum class Environment(
    val baseUrl: String,
    val logLevel: String,
    val debugMode: Boolean,
    val timeoutDetik: Int
) {
    DEVELOPMENT(
        baseUrl = "http://localhost:8080",
        logLevel = "DEBUG",
        debugMode = true,
        timeoutDetik = 60
    ),
    STAGING(
        baseUrl = "https://staging.api.example.com",
        logLevel = "INFO",
        debugMode = true,
        timeoutDetik = 30
    ),
    PRODUCTION(
        baseUrl = "https://api.example.com",
        logLevel = "ERROR",
        debugMode = false,
        timeoutDetik = 15
    );

    companion object {
        fun dariNama(nama: String): Environment =
            entries.find { it.name.equals(nama, ignoreCase = true) }
                ?: throw IllegalArgumentException("Environment tidak dikenal: $nama")

        fun aktif(): Environment {
            val nama = System.getenv("APP_ENV") ?: "DEVELOPMENT"
            return dariNama(nama)
        }
    }
}

val env = Environment.aktif()
println("Menghubungkan ke: ${env.baseUrl}")
println("Log level: ${env.logLevel}")

Companion Object dalam Enum #

enum class Warna(val hex: String, val r: Int, val g: Int, val b: Int) {
    MERAH("#FF0000", 255, 0, 0),
    HIJAU("#00FF00", 0, 255, 0),
    BIRU("#0000FF", 0, 0, 255),
    PUTIH("#FFFFFF", 255, 255, 255),
    HITAM("#000000", 0, 0, 0);

    fun luminance(): Double = 0.2126 * r + 0.7152 * g + 0.0722 * b
    fun isTerang(): Boolean = luminance() > 128.0
    fun warnaTeks(): Warna = if (isTerang()) HITAM else PUTIH

    companion object {
        fun dariHex(hex: String): Warna? =
            entries.find { it.hex.equals(hex, ignoreCase = true) }

        fun dariRGB(r: Int, g: Int, b: Int): Warna? =
            entries.find { it.r == r && it.g == g && it.b == b }

        val warnaCerah: List<Warna>
            get() = entries.filter { it.isTerang() }
    }
}

println(Warna.MERAH.warnaTeks())        // PUTIH (merah gelap, teks putih lebih terbaca)
println(Warna.PUTIH.warnaTeks())        // HITAM
println(Warna.dariHex("#00FF00"))        // HIJAU
println(Warna.warnaCerah)               // [HIJAU, PUTIH]

Serialisasi dan Persistensi Enum #

import com.google.gson.*

enum class Role { ADMIN, EDITOR, VIEWER }

// Menyimpan ke database — gunakan name atau kode khusus
data class User(val nama: String, val role: Role)

// Simpan ke DB sebagai String
fun simpanUser(user: User) {
    val query = "INSERT INTO users VALUES (?, ?)"
    db.execute(query, user.nama, user.role.name)   // "ADMIN", "EDITOR", dst
}

// Baca dari DB
fun bacaUser(baris: ResultSet): User = User(
    nama = baris.getString("nama"),
    role = Role.valueOf(baris.getString("role"))
)

// Serialisasi JSON dengan Gson — enum otomatis jadi String nama
val gson = Gson()
val user = User("Andi", Role.ADMIN)
val json = gson.toJson(user)              // {"nama":"Andi","role":"ADMIN"}
val balik = gson.fromJson(json, User::class.java)

// Jika butuh kode numerik — gunakan ordinal atau custom field
enum class StatusDB(val kode: Int) {
    DRAFT(0), PUBLISHED(1), ARCHIVED(2);

    companion object {
        fun dariKode(kode: Int): StatusDB =
            entries.find { it.kode == kode }
                ?: throw IllegalArgumentException("Kode tidak valid: $kode")
    }
}

// Simpan kode ke DB, bukan nama — lebih kompak dan stabil
fun simpanStatus(status: StatusDB): Int = status.kode
fun bacaStatus(kode: Int): StatusDB = StatusDB.dariKode(kode)

Ringkasan #

  • Enum Kotlin lebih dari sekadar konstanta — bisa memiliki property, method, dan bahkan implementasi berbeda per entry melalui method abstrak. Ini menjadikan enum alat modeling yang kuat.
  • entries (Kotlin 1.9+) adalah pengganti values() yang lebih efisien — mengembalikan List<T> immutable, tidak mengalokasikan array baru setiap dipanggil. Gunakan entries untuk kode baru.
  • when dengan enum bersifat exhaustive saat digunakan sebagai expression — compiler memastikan semua entry ditangani. Jika entry baru ditambahkan ke enum tapi when tidak diupdate, terjadi compile error.
  • Method abstrak per entry memungkinkan setiap enum entry memiliki implementasi berbeda — alternatif bersih untuk Strategy Pattern tanpa perlu kelas terpisah.
  • valueOf(nama) mencari entry berdasarkan nama (case-sensitive) dan melempar exception jika tidak ditemukan. Bungkus dengan runCatching { }.getOrNull() untuk penanganan aman.
  • Enum cocok untuk konstanta dengan struktur sama, state machine sederhana, konfigurasi per environment, dan kategori/tipe yang terbatas. Sealed class lebih tepat saat setiap kasus perlu menyimpan data yang berbeda.
  • Companion object dalam enum berguna untuk factory methods: dariKode(), dariNama(), atau properti turunan seperti warnaCerah — logika yang berkaitan dengan seluruh enum, bukan satu entry.
  • Persistensi enum: simpan name ke database untuk keterbacaan, atau field kustom (kode) untuk kompaknya. Hindari ordinal untuk persistensi — ordinal bisa berubah jika urutan entry diubah.
  • Interface pada enum memastikan semua entry mengimplementasikan kontrak yang sama — berguna untuk polimorfisme dan dependency injection yang lebih fleksibel.
  • Enum mencegah typo dan nilai tidak valid di compile time — selalu lebih baik dari konstanta String atau Int yang bisa berisi nilai apapun.

← Sebelumnya: Result   Berikutnya: Comparator & Sorting →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact