Do you remember enums, containers for a collection of constants? Today we will talk about another kind of container – the one for fixed subclasses: a sealed class or sealed interface. Let's get started.
Basic syntax
To declare a sealed class or interface, we just need to put the sealed modifier in front of the class or interface:
sealed class CustomError
sealed interface CustomErrorsAs sealed classes and sealed interfaces are similar (the only difference is that between a class and an interface), we will continue with sealed class examples only.
A sealed class is abstract, so it can't be instantiated. The following code will give an error:
fun main() {
// Sealed types cannot be instantiated
val customError = CustomError()
}However, of course, you can extend it.
Like with normal classes, you can declare constructors, but constructors in a sealed class must be private or protected:
sealed class CustomError {
constructor(type: String) {} // protected (default)
private constructor(type: String, code: Int) {} // private
public constructor() {} // Public gives error
}You can also use a primary constructor, exactly like in any normal class:
//primary constructor
sealed class CustomError(type: String) Sealed class vs enum
One of the best ways to understand sealed classes is to compare them with enums. Basically, a sealed class is like an enum but with more flexibility. What does this mean? Consider the example below:
enum class Staff(numberOfLessons: Int) {
TEACHER(2), MANAGER("Manager is managing")
}That is not possible with an enum but can be done with a sealed class:
sealed class Staff {
class Teacher(val numberOfLessons: Int) : Staff()
class Manager(val Responsibility: String) : Staff()
object Worker : Staff()
}Enum constants have only one single type, while sealed classes offer multiple instances with greater flexibility. We can conclude that an enum is used to represent a fixed set of values, while a sealed class is a class used to represent a fixed set of subclasses of the type of the given class.
An enum can't inherit from classes or interfaces, while a sealed class can. Consider the example below:
open class Person {
fun whoAmI(name: String): String {
return "I am $name"
}
}
sealed class Staff : Person() {
class Teacher(val numberOfLessons: Int) : Staff()
class Manager(val Responsibility: String) : Staff()
object Worker : Staff()
}
fun main() {
val worker = Staff.Worker
println(worker.whoAmI("Worker"))
}We declared a simple class Person, which has a method, and then a sealed class Staff to extend it, which gives us the power of inheritance we've already talked about. In the main method, we used the method of Person from worker. And that's it.
On the other hand, if we tried to do that with an enum, it would throw an error. The code below won't work:
enum class Staff : Person() {
//...//
}Sealed classes and the when expression
Sealed classes are often used with the when expression, as each class is considered as a case. Let's take an example:
sealed class Staff {
class Teacher(val numberOfLessons: Int) : Staff()
class Manager(val Responsibility: String) : Staff()
object Worker : Staff()
}
fun listTheTasks(staff: Staff) = when (staff) {
is Staff.Teacher -> println("The teacher has ${staff.numberOfLessons} lessons today")
is Staff.Manager -> println("The manager is doing ${staff.Responsibility} today")
Staff.Worker -> println("Worker is fixing the projector for profs in CS, all respect to him.")
}We declared a sealed class Staff with two classes and one object. An object is better in the case of no state. Then we created a function listTheTasks. Notice that is is required in the case of classes and not in the case of an object. There's no else, as we've handled all the cases.
Let's run the function:
fun main() {
val teacher = Staff.Teacher(3)
val worker = Staff.Worker
listTheTasks(teacher)
listTheTasks(worker)
}
// output:
// The teacher has 3 lessons today
// Worker is fixing the projector for profs in CS, all respect to him.Location of direct subclasses
Now, a final important note to keep in mind. Direct subclasses of sealed classes and interfaces must be declared in the same package. On the other hand, this does not apply to indirect subclasses. You may ask what direct and indirect subclasses are. Let's clarify it with a simple example below:
open class B : A() // class B is direct subclass of class A
open class C : B() // class C is indirect subclass of class A and direct subclass of class BIt's direct if there's no class between the parent and the child.
Quoting the official documentation, "All direct subclasses of a sealed class are known at compile time. No other subclasses may appear outside a module/ package within which the sealed class is defined."
Conclusion
In this topic, we've learned that a sealed class, as the name "sealed" implies, restricts class hierarchies, which is useful when we want to represent a fixed set of subclasses.
We've also found out that a sealed class is like an enum but with more flexibility. Also, we've seen how to use it with the when expression, and finally, we've discussed the restriction of class hierarchies in sealed classes. Now, let's get some practice.