Kotlin - Enums
Overview
Enum classes in Kotlin provide a way to define a type-safe set of constants. They're more powerful than Java enums, supporting properties, methods, and implementing interfaces. This tutorial covers enum declarations, advanced patterns, and practical applications.
๐ฏ Learning Objectives:
- Understand enum class declaration and basic usage
- Learn to add properties and methods to enums
- Master enum interfaces and abstract methods
- Explore enum companion objects and extensions
- Apply enums in real-world scenarios like state machines and configuration
Basic Enum Declaration
Enums define a finite set of constants. Each enum constant is an object instance of the enum class.
Simple Enum
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
enum class Priority {
LOW, MEDIUM, HIGH, CRITICAL
}
fun main() {
val direction = Direction.NORTH
println("Direction: $direction") // Direction: NORTH
// Enum properties
println("Name: ${direction.name}") // Name: NORTH
println("Ordinal: ${direction.ordinal}") // Ordinal: 0
// Get all enum values
val allDirections = Direction.values()
println("All directions: ${allDirections.joinToString()}")
// valueOf() method
val priority = Priority.valueOf("HIGH")
println("Priority: $priority") // Priority: HIGH
}
Enums with Properties
enum class Planet(val radius: Double, val mass: Double) {
MERCURY(2.4397e6, 3.3022e23),
VENUS(6.0518e6, 4.8675e24),
EARTH(6.37814e6, 5.97237e24),
MARS(3.3972e6, 6.4185e23),
JUPITER(7.1492e7, 1.8986e27),
SATURN(6.0268e7, 5.6846e26),
URANUS(2.5559e7, 8.6810e25),
NEPTUNE(2.4746e7, 1.0243e26);
// Calculated property
val surfaceGravity: Double
get() = 6.67300E-11 * mass / (radius * radius)
// Method
fun surfaceWeight(mass: Double): Double = mass * surfaceGravity
}
fun main() {
val earthWeight = 75.0
Planet.values().forEach { planet ->
val weight = planet.surfaceWeight(earthWeight)
println("Weight on ${planet.name}: ${"%.2f".format(weight)} kg")
}
// Using specific planet
val mars = Planet.MARS
println("\nMars details:")
println("Radius: ${mars.radius / 1000} km")
println("Mass: ${"%.2e".format(mars.mass)} kg")
println("Surface gravity: ${"%.2f".format(mars.surfaceGravity)} m/sยฒ")
}
Enums with Methods
enum class HttpStatus(val code: Int, val message: String) {
OK(200, "OK"),
CREATED(201, "Created"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
NOT_FOUND(404, "Not Found"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error");
fun isSuccessful(): Boolean = code in 200..299
fun isClientError(): Boolean = code in 400..499
fun isServerError(): Boolean = code in 500..599
fun getFullMessage(): String = "$code $message"
companion object {
fun fromCode(code: Int): HttpStatus? = values().find { it.code == code }
fun getSuccessfulStatuses(): List = values().filter { it.isSuccessful() }
}
}
fun main() {
val statuses = listOf(
HttpStatus.OK,
HttpStatus.NOT_FOUND,
HttpStatus.INTERNAL_SERVER_ERROR
)
statuses.forEach { status ->
println("${status.getFullMessage()}: " +
"Success=${status.isSuccessful()}, " +
"Client Error=${status.isClientError()}, " +
"Server Error=${status.isServerError()}")
}
// Using companion object methods
val status404 = HttpStatus.fromCode(404)
println("\nFound status: ${status404?.getFullMessage()}")
val successStatuses = HttpStatus.getSuccessfulStatuses()
println("Success statuses: ${successStatuses.map { it.name }}")
}
Enums Implementing Interfaces
interface Drawable {
fun draw(): String
}
interface Colorable {
val color: String
}
enum class Shape : Drawable, Colorable {
CIRCLE {
override val color = "Red"
override fun draw() = "Drawing a red circle โญ"
override fun area(radius: Double) = Math.PI * radius * radius
},
SQUARE {
override val color = "Blue"
override fun draw() = "Drawing a blue square ๐ฆ"
override fun area(side: Double) = side * side
},
TRIANGLE {
override val color = "Green"
override fun draw() = "Drawing a green triangle ๐บ"
override fun area(base: Double, height: Double = base) = 0.5 * base * height
};
// Abstract method that each enum constant must implement
abstract fun area(vararg dimensions: Double): Double
// Common method for all enum constants
fun describe(): String = "This is a ${color.lowercase()} ${name.lowercase()}"
}
fun main() {
Shape.values().forEach { shape ->
println(shape.draw())
println(shape.describe())
// Calculate areas with different parameters
val area = when (shape) {
Shape.CIRCLE -> shape.area(5.0)
Shape.SQUARE -> shape.area(4.0)
Shape.TRIANGLE -> shape.area(6.0, 8.0)
}
println("Area: ${"%.2f".format(area)}\n")
}
}
Advanced Enum Patterns
State Machine with Enums
enum class ConnectionState(val canConnect: Boolean, val canDisconnect: Boolean) {
DISCONNECTED(canConnect = true, canDisconnect = false) {
override fun next(): ConnectionState = CONNECTING
override fun getDisplayMessage() = "Not connected"
},
CONNECTING(canConnect = false, canDisconnect = true) {
override fun next(): ConnectionState = CONNECTED
override fun getDisplayMessage() = "Connecting..."
},
CONNECTED(canConnect = false, canDisconnect = true) {
override fun next(): ConnectionState = DISCONNECTING
override fun getDisplayMessage() = "Connected successfully"
},
DISCONNECTING(canConnect = false, canDisconnect = false) {
override fun next(): ConnectionState = DISCONNECTED
override fun getDisplayMessage() = "Disconnecting..."
},
ERROR(canConnect = true, canDisconnect = false) {
override fun next(): ConnectionState = CONNECTING
override fun getDisplayMessage() = "Connection error - tap to retry"
};
abstract fun next(): ConnectionState
abstract fun getDisplayMessage(): String
companion object {
fun fromString(state: String): ConnectionState? {
return values().find { it.name.equals(state, ignoreCase = true) }
}
}
}
class NetworkConnection {
private var currentState = ConnectionState.DISCONNECTED
fun getCurrentState(): ConnectionState = currentState
fun connect(): Boolean {
return if (currentState.canConnect) {
currentState = ConnectionState.CONNECTING
// Simulate connection process
println("๐ ${currentState.getDisplayMessage()}")
// Simulate success/failure
currentState = if (Math.random() > 0.3) {
ConnectionState.CONNECTED
} else {
ConnectionState.ERROR
}
println(when (currentState) {
ConnectionState.CONNECTED -> "โ
${currentState.getDisplayMessage()}"
ConnectionState.ERROR -> "โ ${currentState.getDisplayMessage()}"
else -> currentState.getDisplayMessage()
})
currentState == ConnectionState.CONNECTED
} else {
println("โ Cannot connect in current state: ${currentState.name}")
false
}
}
fun disconnect(): Boolean {
return if (currentState.canDisconnect) {
currentState = ConnectionState.DISCONNECTING
println("๐ ${currentState.getDisplayMessage()}")
currentState = ConnectionState.DISCONNECTED
println("โ
${currentState.getDisplayMessage()}")
true
} else {
println("โ Cannot disconnect in current state: ${currentState.name}")
false
}
}
}
fun main() {
val connection = NetworkConnection()
repeat(5) {
println("--- Attempt ${it + 1} ---")
println("Current state: ${connection.getCurrentState().getDisplayMessage()}")
when (connection.getCurrentState()) {
ConnectionState.DISCONNECTED, ConnectionState.ERROR -> connection.connect()
ConnectionState.CONNECTED -> connection.disconnect()
else -> println("โณ Operation in progress...")
}
println()
}
}
Configuration System with Enums
enum class Environment(
val databaseUrl: String,
val apiUrl: String,
val debugEnabled: Boolean,
val logLevel: String
) {
DEVELOPMENT(
databaseUrl = "jdbc:h2:mem:testdb",
apiUrl = "http://localhost:8080/api",
debugEnabled = true,
logLevel = "DEBUG"
) {
override fun getMaxConnections() = 5
override fun getCacheSize() = 100
},
TESTING(
databaseUrl = "jdbc:h2:mem:testdb",
apiUrl = "http://test-api.example.com/api",
debugEnabled = true,
logLevel = "INFO"
) {
override fun getMaxConnections() = 10
override fun getCacheSize() = 200
},
PRODUCTION(
databaseUrl = "jdbc:postgresql://prod-db:5432/myapp",
apiUrl = "https://api.example.com/api",
debugEnabled = false,
logLevel = "WARN"
) {
override fun getMaxConnections() = 50
override fun getCacheSize() = 1000
};
abstract fun getMaxConnections(): Int
abstract fun getCacheSize(): Int
fun printConfiguration() {
println("=== ${name} Configuration ===")
println("Database URL: $databaseUrl")
println("API URL: $apiUrl")
println("Debug Enabled: $debugEnabled")
println("Log Level: $logLevel")
println("Max Connections: ${getMaxConnections()}")
println("Cache Size: ${getCacheSize()}")
println()
}
companion object {
fun fromEnvironmentVariable(): Environment {
val env = System.getenv("APP_ENV") ?: "DEVELOPMENT"
return values().find { it.name.equals(env, ignoreCase = true) } ?: DEVELOPMENT
}
}
}
// Application configuration class
class AppConfig {
private val environment = Environment.fromEnvironmentVariable()
fun getEnvironment(): Environment = environment
fun isDevelopment(): Boolean = environment == Environment.DEVELOPMENT
fun isProduction(): Boolean = environment == Environment.PRODUCTION
fun getDatabaseConfig(): DatabaseConfig {
return DatabaseConfig(
url = environment.databaseUrl,
maxConnections = environment.getMaxConnections()
)
}
fun getApiConfig(): ApiConfig {
return ApiConfig(
baseUrl = environment.apiUrl,
debugMode = environment.debugEnabled
)
}
}
data class DatabaseConfig(val url: String, val maxConnections: Int)
data class ApiConfig(val baseUrl: String, val debugMode: Boolean)
fun main() {
// Print all environment configurations
Environment.values().forEach { env ->
env.printConfiguration()
}
// Use application configuration
val appConfig = AppConfig()
println("Current environment: ${appConfig.getEnvironment().name}")
println("Is development: ${appConfig.isDevelopment()}")
println("Database config: ${appConfig.getDatabaseConfig()}")
println("API config: ${appConfig.getApiConfig()}")
}
Enum Extensions and Utilities
enum class DayOfWeek {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
companion object {
fun fromString(day: String): DayOfWeek? {
return values().find { it.name.equals(day, ignoreCase = true) }
}
fun workdays(): List = listOf(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY)
fun weekends(): List = listOf(SATURDAY, SUNDAY)
}
}
// Extension functions for enums
fun DayOfWeek.isWorkday(): Boolean = this in DayOfWeek.workdays()
fun DayOfWeek.isWeekend(): Boolean = this in DayOfWeek.weekends()
fun DayOfWeek.next(): DayOfWeek = DayOfWeek.values()[(ordinal + 1) % DayOfWeek.values().size]
fun DayOfWeek.previous(): DayOfWeek = DayOfWeek.values()[(ordinal - 1 + DayOfWeek.values().size) % DayOfWeek.values().size]
// Generic enum utilities
inline fun > randomEnum(): T {
val values = enumValues()
return values[kotlin.random.Random.nextInt(values.size)]
}
fun > T.cycleTo(target: T): List {
val values = this::class.java.enumConstants
val startIndex = this.ordinal
val endIndex = target.ordinal
return if (startIndex <= endIndex) {
values.slice(startIndex..endIndex)
} else {
values.slice(startIndex until values.size) + values.slice(0..endIndex)
}
}
fun main() {
val today = DayOfWeek.WEDNESDAY
println("Today is $today")
println("Is workday: ${today.isWorkday()}")
println("Is weekend: ${today.isWeekend()}")
println("Next day: ${today.next()}")
println("Previous day: ${today.previous()}")
// Using companion object methods
println("\nWorkdays: ${DayOfWeek.workdays()}")
println("Weekends: ${DayOfWeek.weekends()}")
// Random enum
val randomDay = randomEnum()
println("\nRandom day: $randomDay")
// Cycle from Monday to Friday
val workweek = DayOfWeek.MONDAY.cycleTo(DayOfWeek.FRIDAY)
println("Work week: $workweek")
// Cycle from Friday to Tuesday (wrapping around)
val extendedWeekend = DayOfWeek.FRIDAY.cycleTo(DayOfWeek.TUESDAY)
println("Extended weekend: $extendedWeekend")
}
Best Practices
โ
Best Practices:
- Use enums for finite, known sets of constants
- Add meaningful properties and methods to enum constants
- Implement interfaces when enums need polymorphic behavior
- Use companion objects for factory methods and utilities
- Consider sealed classes for more complex hierarchies
โ Common Pitfalls:
- Don't use enums for values that might change or extend
- Avoid heavy computation in enum constructors
- Be careful with enum serialization across versions
- Don't rely on enum ordinal values for persistence
Practice Exercises
- Create a
Currency
enum with exchange rates and conversion methods - Implement a chess piece enum with movement validation
- Design a permission system using enums with hierarchical access levels
- Create a file type enum with MIME types and validation methods
Quick Quiz
- What's the difference between enum classes and sealed classes in Kotlin?
- Can enum constants have different constructor parameters?
- How do you implement different behavior for each enum constant?
Show Answers
- Enums are for finite sets of constants with the same structure; sealed classes are for type hierarchies with different structures.
- Yes, enum constants can have different constructor parameters, as long as they match one of the enum's constructors.
- Override abstract methods in each enum constant using anonymous class syntax or implement interfaces.