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
- At compile time,
enum class X { A, B, C }expands into afinalclass that extendskotlin.Enum<X>, with one instance generated per constant. - Each constant is instantiated exactly once, in declaration order, when the enum class is first initialized — not each time you reference it.
Direction.NORTHalways refers to that same single object. - A constant that supplies a
{ ... }body, likePLUSin theOperationexample, actually compiles to its own anonymous subclass of the enum class, overriding just for that one constant. ordinalis simply the constant’s index in that declaration-order list;entriesandvalues()return the constants in that same order.- A
whenused 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 onordinalrather than a sequence of comparisons. valueOf("NAME")looks the constant up by name in a map the compiler generates and throwsIllegalArgumentExceptionon 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 valueOf — name survives reordering and insertion; ordinal does not.
Best Practices
- Reach for
enum classwhenever a value has a small, fixed set of valid options known at compile time — it beats rawIntcodes or looseStringconstants because the compiler enforces the valid set for you. - Iterate constants with
entries, notvalues()—entriesis a cached list, whilevalues()allocates a new array on every call. - Attach data to constants through the primary constructor rather than maintaining a separate lookup table (a
Mapor a chain ofif/when) keyed by the constant. - Write
whenover an enum as an expression, without a catch-allelse, whenever every case genuinely needs distinct handling — that way adding a new constant later forces you to revisit every relevantwhen. - Never rely on
ordinalfor 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 }returnsnullon no match, whilevalueOfthrows.
Practice Exercises
- Define
enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES }and add acolorproperty that evaluates to"Red"forDIAMONDSandHEARTS, and"Black"forCLUBSandSPADES, using an exhaustivewhen. Loop overSuit.entriesand print each suit’s name and color. - Define
enum class Coin(val cents: Int) { PENNY(1), NICKEL(5), DIME(10), QUARTER(25) }and writefun totalValue(coins: List<Coin>): Intthat sums thecentsof a list of coins. Test it with a list containing two quarters, a dime, and three pennies; the expected total is63. - Define
enum class Vehiclewith anabstract fun maxSpeedKmh(): Intoverridden per constant forBICYCLE,CAR, andPLANEwith plausible values, plus a separate function that uses an exhaustivewhen(noelse) to return whether each vehicleneedsLicense. Print the results for all three constants.
Summary
enum classdefines 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
nameandordinalfrom the implicitkotlin.Enumsuperclass. entries(preferred) andvalueOf(name)are generated for you; the oldervalues()still works but reallocates an array on every call.- Constants can carry constructor arguments and can each override a shared
abstractoropenmember 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
whenexpression over an enum must be exhaustive, giving you a genuine compile-time safety net when new constants are added later. - Never rely on
ordinalfor persisted data or business-critical comparisons — it depends entirely on declaration order and shifts silently if that order changes.
