1. Introduction

In programming, there’s always been a need to differentiate between type constants. In the past, we did so by declaring static variables. Such methods aren’t wrong, but there are better ways to accomplish this. In the Kotlin language, there’s more than one way to achieve this.

In this tutorial, we’ll take a look at the two most commonly used class types for such use cases: the sealed class and the enum class.

2. Sealed Class

Sealed classes provide a hierarchy of classes that have subclasses that we can only declare at compile time.

2.1. Declaration

Let’s start by creating a sealed class to represent different operating systems:

sealed class OsSealed {
    object Linux : OsSealed()
    object Windows : OsSealed()
    object Mac : OsSealed()
}

We can also add a constructor parameter to represent the company that owns the operating system:

sealed class OsSealed(val company: String) {
    object Linux : OsSealed("Open-Source")
    object Windows : OsSealed("Microsoft")
    object Mac : OsSealed("Apple")
}

Note: a sealed class can’t be extended from outside the parent class file.

2.2. Usage

One well-known method of using a sealed class is with the when expression:

when (osSealed) {
    OsSealed.Linux -> println("${osSealed.company} - Linux Operating System")
    OsSealed.Mac -> println("${osSealed.company} - Mac Operating System")
    OsSealed.Windows -> println("${osSealed.company} - Windows Operating System")
}

2.3. Functions

Declaring functions inside of a sealed class is pretty straightforward. If we need to use the function in all subclasses, we can declare them in the main parent class, whereas, if we want to use specific functions with different functionalities, we can do so by creating a function under any object or subclass:

sealed class OsSealed(val releaseYear: Int = 0, val company: String = "") {
    constructor(company: String) : this(0, company)

    object Linux : OsSealed("Open-Source") {
        fun getText(value: Int): String {
            return "Linux by $company - value=$value"
        }
    }

    object Windows : OsSealed("Microsoft") {
        fun getNumber(value: String): Int {
            return value.length
        }
    }

    object Mac : OsSealed(2001, "Apple") {
        fun doSomething(): String {
            val s = "Mac by $company - released at $releaseYear"
            println(s)
            return s
        }
    }

    object Unknown : OsSealed()

    fun getTextParent(): String {
        return "Called from parent sealed class"
    }
}

Functions that are declared inside parent classes can be called directly without casting. On the other hand, functions that are declared inside child objects or subclasses have to be called by explicitly casting them to their equivalent types.

It’s preferable to use the is operator to remove unnecessary boilerplate code inside the when modifier. Doing so will eliminate the need to cast the value of osSealed parameter to the OsSealed.Linux object or other defined objects or subclasses:

assertEquals("Called from parent sealed class", osSealed.getTextParent())
when (osSealed) {
    is OsSealed.Linux -> assertEquals("Linux by Open-Source - value=1", osSealed.getText(1))
    is OsSealed.Mac -> assertEquals("Mac by Apple - released at 2001", osSealed.doSomething())
    is OsSealed.Windows -> assertEquals(5, osSealed.getNumber("Text!"))
    else -> assertTrue(osSealed is OsSealed.Unknown)
}

3. Enum Class

We use an enum class to relate each enum constant with its parent.

3.1. Declaration

Here, we’ve created an enum of different operating systems and, as we can see, it’s very similar to the Java way of declaring an enum:

enum class OsEnum {
    Linux,
    Windows,
    Mac
}

We can also add a constructor parameter to the enum class:

enum class OsEnum(val company: String) {
    Linux("Open-Source"),
    Windows("Microsoft"),
    Mac("Apple")
}

3.2. Usage

Similar to the sealed class, we can use an enum with a when expression, which is used mostly to deal with different scenarios, depending on the enum constant passed:

when (osEnum) {
    OsEnum.Linux -> println("${osEnum.company} - Linux Operating System")
    OsEnum.Mac -> println("${osEnum.company} - Mac Operating System")
    OsEnum.Windows -> println("${osEnum.company} - Windows Operating System")
}

3.3. Functions

There are a couple of ways we can declare a function in an enum class. We can either declare the function as an abstract function and override it in each enum constant, or we can declare it in the parent enum class and use that function with any of the enum constants:

enum class OsEnum(val releaseYear: Int = 0, val company: String = "") {
    Linux(0, "Open-Source") {
        override fun getText(value: Int): String {
            return "Linux by $company - value=$value"
        }
    },
    Windows(0, "Microsoft") {
        override fun getText(value: Int): String {
            return "Windows by $company - value=$value"
        }
    },
    Mac(2001, "Apple") {
        override fun getText(value: Int): String {
            return "Mac by $company - released at $releaseYear"
        }
    },
    Unknown {
        override fun getText(value: Int): String {
            return ""
        }
    };

    abstract fun getText(value: Int): String

    fun getTextParent(): String {
        return "Called from parent enum class"
    }
}

Since we used an abstract function, there can’t be any difference in function names. Therefore, we only need to use the passed osEnum parameter to access the implemented abstract function, as well as the parent declared function:

assertEquals("Called from parent enum class", osEnum.getTextParent())
when (osEnum) {
    OsEnum.Linux -> assertEquals("Linux by Open-Source - value=1", osEnum.getText(1))
    OsEnum.Windows -> assertEquals("Windows by Microsoft - value=2", osEnum.getText(2))
    OsEnum.Mac -> assertEquals("Mac by Apple - released at 2001", osEnum.getText(3))
    else -> assertTrue(osEnum == OsEnum.Unknown)
}

4. Sealed Class vs Enum

We’ve talked about each one in detail, so now let’s look at their differences.

enums are mostly used as constants that relate to each other. They can be paired with some parent functions, as well.

sealed classes are similar to enums but allow more customizations. As described, they’re a mix between enums and abstract classes.

Let’s say we want to add a subclass that doesn’t have a known value or doesn’t need to be implemented.

In a sealed class, we can simply add multiple custom constructors depending on what we need. Furthermore, we can define multiple functions with different names, parameters, and return types.

In an enum class, however, we can’t define different functions in each enum constant. Therefore, even in our Unknown enum constant, we had to implement a method that we didn’t need for that constant, and we also had to pass integer value 0 when initializing Linux and Windows enum constants.

5. Use Cases

enums and sealed classes might seem similar at first glance. And, although enums might seem unnecessary since declaring functions in enum classes is a bit restricted, they still have their use cases.

For example, what if we need to hold some constants and connect them with little to no functionality? The recommended option would be using an enum because it’s simple, straightforward, and does the job.

On the other hand, if we need to have some related constants, each of which holds different data and implements different logic, the best option is to use a sealed class.

6. Conclusion

In this article, we looked at sealed classes and enums, along with how to use them. Additionally, we have discussed the multiple ways of initializing and declaring enums, sealed classes, and their functions. We’ve also talked about when to use an enum and when to use a sealed class. Finally, we discussed which use cases are most suitable for each class.

As always, the source code is available over on GitHub.