Demystifying Kotlin sealed classes

Photo by Marc Reichelt on Unsplash

Sometimes when programming we want to define some constants in a clean, yet useful manner. Perhaps we want to also group them, make them restricted. You have probably heard of enums. Those are great and allow us to have constants. Like so:

enum class Vehicle() {
Car,
Bicycle,
Truck
}

Looks awesome!

But how about when we want to add some more information to the vehicle enum? Like the number of wheels? We can do it like so:

enum class Vehicle(val wheels: Int) {
Car(wheels = 4),
Bicycle(wheels = 2),
Truck(wheels = 4),
}

Now we have a wheel count, which we can get from the enum easily:

val wheels = Car.wheels // 4

But you can already see that there is a limitation to this approach. We can define properties that are mutual for all constant objects. You can add speed, color, or whatever you like. That obviously works for most cases, but sometimes you want to add something more to an object. Something that won’t apply to others. Then maybe you would also like to add an object-specific function to do something with the data inside. That would be useful, right? Here come the Kotlin sealed classes:

sealed class Vehicle(val wheels: Int) {

object Car : Vehicle(wheels = 4) {}

object Bicycle : Vehicle(wheels = 2) {}

object Truck : Vehicle(wheels = 4) {}

}

The above snippet directly translates to the example with enums. But we don’t want something that acts the same, we want to have something that allows us to do more. We have different vehicles here, each can do something different in real life and we want to define that in some way. Enums won’t allow us to, but a sealed class will.

sealed class Vehicle(val wheels: Int) {

object Car : Vehicle(wheels = 4) {

fun getHorseSpeed(): String {
return "100hp"
}

}

object Bicycle : Vehicle(wheels = 2) {

fun getType(): Int {
return 1
}

}

object Truck : Vehicle(wheels = 4) {

fun getMaxLoad(): Int {
return 3000
}

}

}

Do you see? Now we have different functions in different classes, which return us information about different vehicles. Neat!

But what if we expand this a bit more? We are working with classes here, right? Of course! Say we have an unknown vehicle and would like to have it almost like a template for the future so expand from. You can define an open class as part of a sealed class. Like so:

sealed class Vehicle(val wheels: Int) {

object Car : Vehicle(wheels = 4) {

fun getHorseSpeed(): String {
return "100hp"
}

}

object Bicycle : Vehicle(wheels = 2) {

fun getType(): Int {
return 1
}

}

object Truck : Vehicle(wheels = 4) {

fun getMaxLoad(): Int {
return 3000
}

}

open class UnknownVehicle(wheels: Int) : Vehicle(wheels) {}
}

Keep in mind though that you can only define subclasses within the same file and only in scope that is aware of the sealed class.

Can we define an abstract function to force the subclasses to implement it? Sure we can!

sealed class Vehicle(val wheels: Int) {

object Car : Vehicle(wheels = 4) {

fun getHorseSpeed(): String {
return "100hp"
}

override fun getParkingSpotNumber(): Int {
return 2
}
}

object Bicycle : Vehicle(wheels = 2) {

fun getType(): Int {
return 1
}

override fun getParkingSpotNumber(): Int {
return 5
}
}

object Truck : Vehicle(wheels = 4) {

fun getMaxLoad(): Int {
return 3000
}

override fun getParkingSpotNumber(): Int {
return 45
}
}

open class UnknownVehicle(wheels: Int) : Vehicle(wheels) {
override fun getParkingSpotNumber(): Int? {
return null
}
}

abstract fun getParkingSpotNumber(): Int?
}

Here it works in an ordinary fashion, we have an abstract function, that is like a blueprint, that has to be implemented in the classes/objects that extend from the class that has it.

The real beauty of sealed classes appears when we use them with when statements. Each type in the sealed class acts as a case and you don’t even need to use the “is” keyword if it is defined as an object. As well as that, sealed classes by definition are restricted and hence why you can omit the else branch when you define all the cases. Like so:

fun getParkingSpot(vehicle: Vehicle): Int? {
return when (vehicle) {
Vehicle.Bicycle -> vehicle.getParkingSpotNumber()
Vehicle.Car -> vehicle.getParkingSpotNumber()
Vehicle.Truck -> vehicle.getParkingSpotNumber()
is Vehicle.UnknownVehicle -> vehicle.getParkingSpotNumber()
}
}

Some things to consider when using a sealed class:

  • Sealed classes don’t have public constructors, but they do have private ones
  • Sealed classes can have subclasses, but those have to be either nested within the definition or in the same file
  • Sealed classes can’t be instantiated directly

I hope sealed classes are much clearer to you than to me when I first saw them. Comments and/or suggestions are welcome in the comments.

Happy holidays!


Demystifying Kotlin sealed classes was originally published in Kt. Academy on Medium, where people are continuing the conversation by highlighting and responding to this story.

Continue ReadingDemystifying Kotlin sealed classes

End of content

No more pages to load