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 :
vehiclesFor(Air)
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
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)
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.