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

  1. Create a Currency enum with exchange rates and conversion methods
  2. Implement a chess piece enum with movement validation
  3. Design a permission system using enums with hierarchical access levels
  4. Create a file type enum with MIME types and validation methods

Quick Quiz

  1. What's the difference between enum classes and sealed classes in Kotlin?
  2. Can enum constants have different constructor parameters?
  3. How do you implement different behavior for each enum constant?
Show Answers
  1. Enums are for finite sets of constants with the same structure; sealed classes are for type hierarchies with different structures.
  2. Yes, enum constants can have different constructor parameters, as long as they match one of the enum's constructors.
  3. Override abstract methods in each enum constant using anonymous class syntax or implement interfaces.