Kotlin Training Program

DOWNLOAD APP

FEEDBACK

Enum class

Enum (Enumeration) class is a special type of class that is used to define fixed set of values for a given entity.

Enum classes can help make the code more readable and reduce the possibility of errors due to typos or incorrect inputs. Let us understand the need of enum class with the help of an example.

Need

Consider the following User class with two string fields - name & gender :

 class User(
    val name: String,
    val gender: String
)
 

We can create an User object as :

 val u1 = User("A", "Male")
 

Note that gender can have only two possible values - Male & Female. gender is a string so any other input can also be entered. If we enter a value other than that, it is accepted :

 val u2 = User("A", "Mael")
 

To make sure we receive only valid values for gender, we can add a check in init block :

 class User(
    val name: String,
    val gender: String
) {
    init {
        if (gender !in listOf("Male", "Female")) {
            error("Invalid gender!")
        }
    }
}
 

This manual check is an overhead. Moreover, it makes our program error-prone. On invalid inputs, the program will crash. Lets solve this problem using enum class.

Gender entity is a perfect example where we have only a fixed set of possible values - Male & Female. So instead of using string, we can use Gender enum class :

 enum class Gender { Male, Female }
 

The possible values for Gender are defined inside the enum class body separated by commas ,.

We can update the User class to use this :

 class User(
    val name: String,
    val gender: Gender
)
 

Creating an object of User class :

 val u1 = User("A", Gender.Male)
 

Values of enum class can be accessed using Dot notation ..

By defining an enum class for Gender, we can ensure that no typos or incorrect inputs are accepted. It also makes the code more readable and maintainable.

Basics

Defining

We define an enum class using the enum keyword :

 enum class /* class name */ {
		// values separated by commas
}

// Example :
enum class Day {
		Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}
 

Usage

All the values defined for enum class are objects (singleton). They are supposed to be used directly using dot notation . & not instantiated :

 /* EnumClassName */./* ValueName */

// Correct usage
val today = Day.Monday

// Incorrect usage
val today = Day.Monday()
 

Also, enum class itself can not be instantiated:

 val today = Day() // Invalid!
 

Data members

Enum class can also have data members. For example, Day enum class can have a field - abbr i.e. abbreviation (short name) for each day :

 enum class Day(
		val abbr: String
) {
		// Values...
}
 

The data members then have to be initialized by all the values of enum class :

 enum class Day(
    val abbr: String
) {
    Monday("Mon"),
    Tuesday("Tue"),
    Wednesday("Wed"),
    Thursday("Thu"),
    Friday("Fri"),
    Saturday("Sat"),
    Sunday("Sun")
}
 

Values default members

Each enum class value has two properties defined by default - ordinal: Int & name: String.

  • ordinal is the index of the enum value

     println(Day.Wednesday.ordinal) // Prints "2"
     
  • name is the string representation of the enum value

     println(Day.Wednesday.name) // Prints "Wednesday"
        
    // Printing the enum value directly also prints its name
    println(Day.Wednesday) // Prints "Wednesday"
     

These are defined only for values and not the class itself. So, following is invalid :

 println(Day.ordinal) // Invalid!
println(Day.name)    // Invalid!
 

Class default members

Every enum class has two function defined by default - values() and valueOf() :

  • values() function returns an array of all the values of the enum class. It can used to iterate over all the values.

     println(Day.values().contentToString())
    // Prints : "[Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday]"
     
  • valueOf() function is used to parse a string as an enum value. It throws IllegalArgumentException in case of invalid values.

     println(Day.valueOf("Sunday"))  // Prints "Sunday"
    println(Day.valueOf("Sundays")) // Throws error
     

These are defined only for enum class and not the values. So, following is invalid :

 println(Day.Monday.values())          // Invalid!
println(Day.Sunday.valueOf("Monday")) // Invalid!
 

Member functions

Similar to normal classes, Enum classes can also have member functions. For example, we can define a nextDay() function for Day enum class :

 enum class Day {
    Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday;

    fun nextDay() = values()[(ordinal + 1) % 7]
}
 

Note that a semicolon ; needs to be entered after the values of enum class.

We can then use this function on all values (not class) :

 Day.values().forEach {
    println("$it -> ${it.nextDay()}")
}

// println(Day.nextDay()) // Invalid!

/* Output :

Monday -> Tuesday
Tuesday -> Wednesday
Wednesday -> Thursday
Thursday -> Friday
Friday -> Saturday
Saturday -> Sunday
Sunday -> Monday
 */
 

Member functions can also be defined as open or abstract :

 enum class Day(
    val abbr: String
) {
    Monday("Mon") {
        override fun isSpecial() = true
    },
    Tuesday("Tue"),
    Wednesday("Wed"),
    Thursday("Thu"),
    Friday("Fri"),
    Saturday("Sat"),
    Sunday("Sun");

    fun nextDay() = values()[(ordinal + 1) % 7]
    
    open fun isSpecial() = false
}
 

Exhaustive when statements

when statements used with enum class, can be exhaustive without defining an extra else branch.

Let us understand this property with the help of an example.

Problem

Enums can be easily replaced by plain inheritance. Consider an example of a travel company that shows travel routes via multiple travel mediums - Air, Land and Water. Each medium has multiple vehicle options like [Train, Bus, Car, Bike] for Land. We can define an Enum class for TravelMedium as :

 enum class TravelMedium {
    Air, Land, Water
}
 

But the same can also be represented using inheritance :

 open class TravelMedium
object Air: TravelMedium()
object Land: TravelMedium()
object Water: TravelMedium()
 

Then why do we need Enum classes? One thing is it makes the code look very concise. Another most useful property that Enum classes provide is Exhaustiveness. Lets see what it is.

Lets define a function vehiclesFor(travelMedium: TravelMedium): List<String> that takes a TravelMedium as input and returns the possible vehicle options for it.

Example - Air (Input) → [Rocket, Aeroplane, Private Jet] (Output).

For Inheritance case, we can define it as :

 fun vehiclesFor(travelMedium: TravelMedium): List<String> {
    return when (travelMedium) {
        Air -> listOf("Rocket", "Aeroplane", "Private Jet")
        Land -> listOf("Train", "Bus", "Car", "Bike")
        Water -> listOf("Boat", "Ship", "Cruise", "Submarine")
    }
}
 

The above code does not run because compiler complains that when statement should be exhaustive. Exhaustive means for all possible values of travelMedium, we need to define a branch. It might seem like we have defined a branch for all possible values - Air, Land and Water. What’s left? Following cases are yet to be handled :

  • TravelMedium is a class itself, so it can be instantiated and passed to this function :

     // Normal usage
    vehiclesFor(Air)
    
    // TravelMedium object passed
    vehiclesFor(TravelMedium())
     

    So, branch for handling this TravelMedium() input is absent. We can avoid this input altogether by replacing open class with interface :

     interface TravelMedium
    object Air: TravelMedium
    object Land: TravelMedium
    object Water: TravelMedium
    
    // ERROR! TravelMedium can't be instantiated
    vehiclesFor(TravelMedium())
     

    But still the compiler complains for exhaustive when statements. Let’s see what else is remaining.

  • Any other derived class of TravelMedium which maybe defined in future or is already defined in another file also needs to handled. We are sure that there are only 3 derived classes - Air, Land and Water. But the compiler isn’t smart enough to infer that TravelMedium has only 3 derived classes. when statement needs one more branch for TravelMedium’s future derived classes. We can add a simple else branch to resolve this error :

     fun vehiclesFor(travelMedium: TravelMedium): List<String> {
        return when (travelMedium) {
            Air -> listOf("Rocket", "Aeroplane", "Private Jet")
            Land -> listOf("Train", "Bus", "Car", "Bike")
            Water -> listOf("Boat", "Ship", "Cruise", "Submarine")
            else -> error("vehicles for TravelMedium ${travelMedium.javaClass.simpleName} is not yet defined!")
        }
    }
     

    The error now disappears, but we unknowingly introduced another major error! Lets see how.

Suppose we define one more medium in future :

 object Space: TravelMedium
 

But forget to add a new branch corresponding to Space in the when statement. The compiler does not complain about this because else branch is already defined.

So, when we pass Space to the vehiclesFor() function, it throws a runtime error :

 vehiclesFor(Space) // Throws error
 

This is a common source of error. When defining a new derived class, we have to add the corresponding branch to all when statements. But we may forget to do so, compiler won’t complain and code will run. Only when we get a runtime error, we’ll know that we have to add another branch to when statement.

Imagine a large-scale software project where tens of such when statement might be there. None of them will show compile time error but we may encounter runtime errors if a new derived class is declared.

Solution

Solution is enum class.

 enum class TravelMedium {
    Air, Land, Water
}
 

We replace inheritance with enum class and remove the else branch from when statement :

 fun vehiclesFor(travelMedium: TravelMedium): List<String> {
    return when (travelMedium) {
        TravelMedium.Air -> listOf("Rocket", "Aeroplane", "Private Jet")
        TravelMedium.Land -> listOf("Train", "Bus", "Car", "Bike")
        TravelMedium.Water -> listOf("Boat", "Ship", "Cruise", "Submarine")
    }
}
 

Surprise! The compiler no longer complains about exhaustive when statement. How did this happen? Just by using enum class, the compiler now knows all the possible values for TravelMedium. And because we defined a branch for each possible value, its happy. Moreover, enum class itself can’t be instantiated so we need not handle this case also :

 vehiclesFor(TravelMedium()) // Invalid!
 

The best part is when we add a new TravelMedium :

 enum class TravelMedium {
    Air, Land, Water, /* New : */ Space 
}
 

when statement starts showing compile time error for exhaustiveness :

 fun vehiclesFor(travelMedium: TravelMedium): List<String> {
    return /* ERROR :- */ when (travelMedium) {
        TravelMedium.Air -> listOf("Rocket", "Aeroplane", "Private Jet")
        TravelMedium.Land -> listOf("Train", "Bus", "Car", "Bike")
        TravelMedium.Water -> listOf("Boat", "Ship", "Cruise", "Submarine")
    }
}
 

This is great! We need not run the program & wait for runtime errors to find such when statements. Just by running, we’ll know about them at compile time itself. So, the scope for error is now eliminated. For enum classes, compiler is smart enough to know all the possible values.