Enum Classes

An enum class in Kotlin defines a fixed, named set of possible values for a type — think of it as a way to say “this variable can only ever be one of these specific options,” checked by the compiler. Kotlin enums are far more powerful than a simple list of names: each constant is a full singleton object that can carry its own properties, implement interfaces, and even override methods with a different body per constant. They replace error-prone patterns like raw integer codes or loose string constants with something the compiler actively verifies for you, including exhaustiveness checks in when expressions.

Overview / How it works

Declaring enum class Direction { NORTH, SOUTH, EAST, WEST } tells the compiler to generate a class named Direction with exactly four instances — NORTH, SOUTH, EAST, and WEST — created once, at class-loading time, and shared everywhere they’re referenced. Each constant is a genuine singleton object of type Direction, not just a label, so comparing two enum values with == (structural equality) and === (referential equality) will always agree for enums, because there is only ever one instance of each constant.

Under the hood, every enum class implicitly extends the abstract class kotlin.Enum<T>, which supplies two read-only properties on every constant: name (the constant’s identifier as a String) and ordinal (its zero-based position in declaration order). The compiler also generates a companion object with valueOf(name: String) — which returns the matching constant or throws IllegalArgumentException if no constant has that name — and an entries property (the modern replacement for the older values() function) that exposes all constants as an immutable, cheaply reused List. Prefer entries over values() in new code: values() allocates a brand-new array on every call, while entries is computed once and cached.

Enum constants aren’t limited to bare names. You can give the enum class a constructor and pass arguments to each constant, so every constant carries its own data — a country’s dialing prefix, a planet’s mass, an HTTP status’s numeric code. You can also declare properties and functions in the class body that every constant shares, and, going further, give an individual constant its own function body that overrides an abstract or open member — this is how you model “one type, several distinct behaviors” without a chain of if/else if or a when scattered through the codebase. Enum classes can also implement one or more interfaces, which pairs especially well with when: because the compiler knows the complete, closed set of an enum’s constants at compile time, a when expression that switches on an enum and omits a constant (with no else) simply fails to compile — a safety net a chain of if statements can never give you.

interface HasLabel {
    val label: String
}

enum class Status(override val label: String) : HasLabel {
    ACTIVE("Active"),
    INACTIVE("Inactive")
}

The snippet above shows an enum class implementing an interface: Status promises every constant a label, and each constant supplies its own value through the primary constructor. Because HasLabel is satisfied, a Status constant can be passed anywhere a HasLabel is expected, right alongside other classes that implement the same interface.

Syntax

The general shape of an enum class declaration:

enum class EnumName(val property1: Type1, val property2: Type2) : InterfaceName {
    CONSTANT_A(value1, value2) {
        override fun someMethod() { /* per-constant body */ }
    },
    CONSTANT_B(value1, value2);

    fun sharedMethod() { /* available on every constant */ }
    abstract fun someMethod()
}
Part Meaning
enum class Keyword pair that declares an enumeration type.
(val property1: Type1, ...) Optional primary constructor; every constant must supply matching constructor arguments.
: InterfaceName Optional — an enum class may implement one or more interfaces.
CONSTANT_A(value1, value2) { ... } A constant with its own body, overriding an abstract or open member just for that constant.
trailing ; Required after the last constant only when the class body has further members after it.
abstract fun someMethod() Forces every constant that doesn’t override it inline to be given a default implementation instead.

Examples

Example 1: A basic enum class

enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

fun main() {
    val heading = Direction.NORTH
    println(heading)
    println(heading.name)
    println(heading.ordinal)
    println(Direction.valueOf("EAST"))
}

Output:

NORTH
NORTH
0
EAST

Printing heading directly calls the generated toString(), which returns the constant’s name by default. name gives that same text as an explicit property, and ordinal is 0 because NORTH is declared first. Direction.valueOf("EAST") looks up the constant by its exact name at runtime; pass a name that doesn’t match any constant and it throws IllegalArgumentException instead of returning null.

Example 2: Constants with their own data and a shared method

enum class Planet(val massKg: Double, val radiusM: Double) {
    MERCURY(3.303e+23, 2.4397e6),
    VENUS(4.869e+24, 6.0518e6),
    EARTH(5.976e+24, 6.37814e6);

    fun surfaceGravity(): Double {
        val g = 6.67300E-11
        return g * massKg / (radiusM * radiusM)
    }
}

fun main() {
    for (planet in Planet.entries) {
        println("${planet.name} gravity: ${"%.2f".format(planet.surfaceGravity())}")
    }
}

Output:

MERCURY gravity: 3.70
VENUS gravity: 8.87
EARTH gravity: 9.80

Each Planet constant carries two Double values supplied through the primary constructor — this is data attached directly to the constant, not looked up from a separate map. surfaceGravity() is defined once in the class body and shared by every constant, since the formula is the same for all of them; only the inputs differ. The loop uses Planet.entries, the modern, allocation-free way to iterate every constant in declaration order (it replaces the older values() function, which built a fresh array on every call).

Example 3: One method, a different body per constant

enum class Operation {
    PLUS {
        override fun apply(a: Int, b: Int) = a + b
    },
    MINUS {
        override fun apply(a: Int, b: Int) = a - b
    },
    TIMES {
        override fun apply(a: Int, b: Int) = a * b
    };

    abstract fun apply(a: Int, b: Int): Int

    val symbol: String
        get() = when (this) {
            PLUS -> "+"
            MINUS -> "-"
            TIMES -> "*"
        }
}

fun main() {
    for (op in Operation.entries) {
        println("6 ${op.symbol} 3 = ${op.apply(6, 3)}")
    }
}

Output:

6 + 3 = 9
6 - 3 = 3
6 * 3 = 18

Each constant of Operation supplies its own body implementing the abstract fun apply, so calling op.apply(6, 3) dispatches to a different calculation depending on which constant op actually is — no when or if chain needed at the call site. Internally, a constant with a body like this compiles to its own anonymous subclass of Operation. Notice that the symbol property’s when (this) block can refer to PLUS, MINUS, and TIMES unqualified — sibling constants are in scope inside the class body — and needs no else branch because all three constants are covered.

How it works step by step

  1. At compile time, enum class X { A, B, C } expands into a final class that extends kotlin.Enum<X>, with one instance generated per constant.
  2. Each constant is instantiated exactly once, in declaration order, when the enum class is first initialized — not each time you reference it. Direction.NORTH always refers to that same single object.
  3. A constant that supplies a { ... } body, like PLUS in the Operation example, actually compiles to its own anonymous subclass of the enum class, overriding just for that one constant.
  4. ordinal is simply the constant’s index in that declaration-order list; entries and values() return the constants in that same order.
  5. A when used as an expression over an enum is checked against every constant known at compile time; on the JVM it compiles down to an efficient jump table keyed on ordinal rather than a sequence of comparisons.
  6. valueOf("NAME") looks the constant up by name in a map the compiler generates and throws IllegalArgumentException on no match. There’s no way to construct a new instance yourself — the constructor of an enum class is implicitly private.

Common Mistakes

Mistake 1: A non-exhaustive when expression

A when used as an expression (its result is assigned, returned, or printed directly) must cover every constant. Forgetting one is a compile error, not a runtime surprise — but only when when is used as an expression rather than a statement:

enum class TrafficLight { RED, YELLOW, GREEN }

fun action(light: TrafficLight): String = when (light) {
    TrafficLight.RED -> "Stop"
    TrafficLight.GREEN -> "Go"
}

This fails to compile with an error along the lines of “'when' expression must be exhaustive, add necessary 'YELLOW' branch or 'else' branch” because YELLOW is never handled. Add the missing branch (or an else if some cases genuinely don’t matter):

enum class TrafficLight { RED, YELLOW, GREEN }

fun action(light: TrafficLight): String = when (light) {
    TrafficLight.RED -> "Stop"
    TrafficLight.YELLOW -> "Slow down"
    TrafficLight.GREEN -> "Go"
}

fun main() {
    println(action(TrafficLight.YELLOW))
}

Output:

Slow down

This is one of Kotlin’s most valuable safety nets: if you later add a fourth TrafficLight constant, every exhaustive when expression built on it will fail to compile until you decide what the new case should do — you can’t forget a branch by accident.

Mistake 2: Using ordinal for business logic

ordinal reflects declaration order, not meaning. Code that leans on it for comparisons looks correct until someone edits the enum:

enum class Priority { LOW, MEDIUM, HIGH }

fun isUrgent(p: Priority): Boolean = p.ordinal >= 2

fun main() {
    println(isUrgent(Priority.HIGH))
    println(isUrgent(Priority.MEDIUM))
}

Output:

true
false

This compiles and works today only because HIGH happens to sit at index 2. Insert a new constant such as CRITICAL above HIGH, or simply reorder the list, and isUrgent silently starts returning the wrong answer for constants whose meaning never changed. Model the intent explicitly instead, so the logic can’t drift out of sync with declaration order:

enum class Priority(val isUrgent: Boolean) {
    LOW(false),
    MEDIUM(false),
    HIGH(true)
}

fun main() {
    println(Priority.HIGH.isUrgent)
    println(Priority.MEDIUM.isUrgent)
}

Output:

true
false

The same rule applies to persistence: never store ordinal in a database column or serialized file as the representation of a constant. Store name instead, and look constants back up with valueOfname survives reordering and insertion; ordinal does not.

Best Practices

  • Reach for enum class whenever a value has a small, fixed set of valid options known at compile time — it beats raw Int codes or loose String constants because the compiler enforces the valid set for you.
  • Iterate constants with entries, not values()entries is a cached list, while values() allocates a new array on every call.
  • Attach data to constants through the primary constructor rather than maintaining a separate lookup table (a Map or a chain of if/when) keyed by the constant.
  • Write when over an enum as an expression, without a catch-all else, whenever every case genuinely needs distinct handling — that way adding a new constant later forces you to revisit every relevant when.
  • Never rely on ordinal for comparisons, sorting by “severity,” or persistence — reordering or inserting a constant silently changes its value.
  • Implement interfaces on an enum class when its constants need to be used polymorphically alongside other implementations of the same interface.
  • Keep per-constant method bodies short; if a single constant’s override grows into real business logic, consider a sealed class hierarchy instead, which gives each variant its own file and more room to grow.
  • When parsing an enum constant from external input (JSON, user text, a config file), guard the lookup — entries.find { it.name == raw } returns null on no match, while valueOf throws.

Practice Exercises

  • Define enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES } and add a color property that evaluates to "Red" for DIAMONDS and HEARTS, and "Black" for CLUBS and SPADES, using an exhaustive when. Loop over Suit.entries and print each suit’s name and color.
  • Define enum class Coin(val cents: Int) { PENNY(1), NICKEL(5), DIME(10), QUARTER(25) } and write fun totalValue(coins: List<Coin>): Int that sums the cents of a list of coins. Test it with a list containing two quarters, a dime, and three pennies; the expected total is 63.
  • Define enum class Vehicle with an abstract fun maxSpeedKmh(): Int overridden per constant for BICYCLE, CAR, and PLANE with plausible values, plus a separate function that uses an exhaustive when (no else) to return whether each vehicle needsLicense. Print the results for all three constants.

Summary

  • enum class defines a fixed, compiler-checked set of singleton constants — comparing two constants with == and === always agrees, since each one exists only once.
  • Every constant automatically gets name and ordinal from the implicit kotlin.Enum superclass.
  • entries (preferred) and valueOf(name) are generated for you; the older values() still works but reallocates an array on every call.
  • Constants can carry constructor arguments and can each override a shared abstract or open member with their own body, compiling to a per-constant anonymous subclass.
  • Enum classes can implement interfaces, letting constants be used polymorphically wherever that interface is expected.
  • A when expression over an enum must be exhaustive, giving you a genuine compile-time safety net when new constants are added later.
  • Never rely on ordinal for persisted data or business-critical comparisons — it depends entirely on declaration order and shifts silently if that order changes.