Sealed & Enum class in Kotlin
Amit Nadiger
Polyglot(Rust??, C++ 11,14,17,20, C, Kotlin, Java) Android TV, Cas, Blockchain, Polkadot, UTXO, Substrate, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Engineering management.
A sealed class in Kotlin is a special type of class that restricts the types of classes or data types that can inherit from it. It is used to create a closed hierarchy of classes or data types where all possible subclasses or subtypes are known at compile time.
i.e No other subclasses may appear outside the module and package within which the sealed class is defined. For example, third-party clients can't extend your sealed class in their code. Thus, each instance of a sealed class has a type from a limited set that is known when this class is compiled.
- A sealed class cannot be instantiated. Hence, are implicitly abstract.
- Constructors of sealed classes can have one of two?visibilities:?protected?(by default) or?private:
- Sealed classes are important in ensuring?type safety?by restricting the set of types at compile-time only.
One of the main advantages of using sealed classes is that they can help enforce exhaustive when expressions, which means that the compiler will warn you if you forget to handle a particular case or condition. This can help prevent runtime errors and make your code more reliable.
To define a sealed class in Kotlin, you use the sealed keyword before the class definition, like this:
sealed class Shape {
? ? class Circle(val radius: Double) : Shape()
? ? class Rectangle(val width: Double, val height: Double) : Shape()
}
class Triangle(val base: Double, val height: Double) : Shape() {
? ? class TestShape():Shape() { // This is also allowed?
? ? ? ??
? ? }
}
fun calculateArea(shape: Shape): Double = when (shape) {
? ? is Shape.Circle -> Math.PI * shape.radius * shape.radius
? ? is Shape.Rectangle -> shape.width * shape.height
? ? is Triangle -> 0.5 * shape.base * shape.height
? ? is Triangle.TestShape -> 0.5
}
fun main() {
? ? val circle = Shape.Circle(5.0)
? ? val rectangle = Shape.Rectangle(4.0, 6.0)
? ? val triangle = Triangle(3.0, 4.0)
? ? val testShape = Triangle.TestShape()
? ??
? ? val circleArea = calculateArea(circle)
? ? val rectangleArea = calculateArea(rectangle)
? ? val triangleArea = calculateArea(triangle)
? ? val testShapeArea = calculateArea(testShape)
? ??
? ? println("Circle area: $circleArea")
? ? println("Rectangle area: $rectangleArea")
? ? println("Triangle area: $triangleArea")
? ? println("testShapeArea area: $testShapeArea")
}
/*
Op =>
Circle area: 78.53981633974483
Rectangle area: 24.0
Triangle area: 6.0
testShapeArea area: 0.5
*/
In the above example, we define a sealed class Shape with three subclasses Circle, Rectangle, and Triangle. We then define a function calculateArea that takes a Shape object and returns its area based on its type. We use the when expression to match on the type of the Shape object and perform the appropriate calculation.
In the main function, we create instances of each Shape subclass and call calculateArea on each one. The output is the calculated area for each shape.
Note that the Shape class is sealed, which means that all of its subclasses must be defined within the same pkg or same module as the sealed class itself. This allows us to exhaustively match on all possible Shape subclasses in the when expression. If a new subclass of Shape is defined outside of this file, the compiler will give an error.
Advantages of Sealed Class:
- Sealed classes are powerful when it comes to modeling a fixed set of types.
- They can be used for modeling state in finite state machine patterns.
- They can be used to make sure that all possible cases are handled in when expressions.
- They provide better type safety and error handling compared to using plain classes and interfaces.
- Sealed classes make the code more readable and maintainable.
Disadvantages of Sealed Class:
- Sealed classes can make code more complex and harder to understand if used inappropriately.
- Sealed classes can also limit extensibility of the code.
Suitable scenarios for using Sealed Class:
- When modeling a finite set of types.
- When implementing state machines.
- When modeling domain objects with a limited set of possible states.
What are sealed Interfaces?
Sealed interfaces were introduced in Kotlin 1.5 as an extension of the sealed classes feature. They share most of the characteristics of sealed classes and provide an additional layer of abstraction for creating hierarchical data types.
A sealed interface is declared using the 'sealed' keyword before the interface keyword. Let's take a look at an example:
sealed interface Shape {
? ? fun calculateArea(): Double
}
class Circle(val radius: Double) : Shape {
? ? override fun calculateArea(): Double {
? ? ? ? return Math.PI * radius * radius
? ? }
}
class Rectangle(val width: Double, val height: Double) : Shape {
? ? override fun calculateArea(): Double {
? ? ? ? return width * height
? ? }
}
fun main() {
? ? val circle = Circle(5.0)
? ? val circleArea = circle.calculateArea()
? ? println("Area of circle with radius ${circle.radius} is $circleArea")
? ? val rectangle = Rectangle(3.0, 4.0)
? ? val rectangleArea = rectangle.calculateArea()
? ? println("Area of rectangle with width ${rectangle.width} and height ${rectangle.height} is $rectangleArea")
}
In this example, we have a sealed interface Shape that defines a single abstract method calculateArea(). We then create two classes, Circle and Rectangle, that implement this method and calculate their respective areas.
One advantage of using sealed interfaces is that they allow us to create hierarchical data types in a type-safe manner. For example, we can create a function that takes a Shape as an argument and performs some operation on it:
fun printShapeArea(shape: Shape) {
? ? val area = shape.calculateArea()
? ? println("Area of ${shape::class.simpleName} is $area")
}
fun main() {
? ? val circle = Circle(5.0)
? ? printShapeArea(circle)
? ? val rectangle = Rectangle(3.0, 4.0)
? ? printShapeArea(rectangle)
}
This function can be used with any class that implements the Shape interface, allowing us to add more shapes to our program in the future without modifying the function.
What is difference between normal interface & sealed interface in Kotlin?
A normal interface in Kotlin can be implemented by any class or object that provides an implementation for all its members. On the other hand, a sealed interface is designed to be implemented only by a finite set of classes that are defined within the same file or pkg or module as the sealed interface. These classes are declared as sealed and must be subclasses of the sealed interface. The number of subclasses is limited and known at compile time.
The key difference between a normal interface and a sealed interface is the ability to restrict the number of implementing classes. Sealed interfaces provide additional safety by ensuring that all the implementing classes are known at compile-time and defined within the same pkg or module. This makes it easier to reason about the code and prevents unexpected behavior that could result from unknown implementing classes.
Another difference is that sealed interfaces can define their own hierarchy of classes, while normal interfaces do not. This allows for more precise control over the structure of the code and can help enforce design constraints.
One potential disadvantage of sealed interfaces is that they can make the code more complex if overused. Since a sealed interface can have multiple implementations, it may be harder to reason about the behavior of a function that takes a sealed interface as an argument.
Like sealed classes, a sealed interface restricts the types of its implementers. In the above example, any class that implements the Animal interface must either be a Mammal or a Bird, and any class that implements the Mammal interface must either be a Dog or a Cat.
One of the main benefits of using a sealed interface is that it allows you to define a restricted hierarchy of interfaces for modeling data. This means that you can ensure that a certain set of interfaces are the only ones that can be used to represent a particular type of data.
Advantages of Sealed Interface:
- Sealed interfaces can be used to define a set of related interfaces with a limited set of implementations.
- They can be used to improve the type safety of your code and reduce the chance of runtime errors.
- Sealed interfaces are useful when you want to define a fixed set of interfaces with a limited set of implementations.
Disadvantages of Sealed Interface:
- Sealed interfaces can make code more complex and harder to understand if used inappropriately.
- Sealed interfaces can also limit extensibility of the code.
Suitable scenarios for using Sealed Interface:
- When you want to define a set of related interfaces with a limited set of implementations.
- When you want to improve the type safety of your code.
领英推è
What is the motivation behind adding the sealed keyword in Kotlin?
The motivation behind adding sealed classes and interfaces to the Kotlin language is to provide a better way to express restricted class hierarchies. It provides a way to define a closed set of subclasses that can inherit from a parent class or implement an interface. Sealed classes and interfaces ensure that all the possible subclasses of a parent class or interface are known and defined in a single place and at the compile time.
This helps in improving the code quality and maintainability, as it provides better compile-time checks and avoids runtime errors that may occur due to unhandled cases. Sealed classes and interfaces are often used in combination with when expressions, which helps in creating exhaustive expressions that handle all possible cases.
For example, imagine a scenario where we need to implement a payment system in an e-commerce application. We have a Payment class and different payment methods like Credit Card, Debit Card, PayPal, and others. We can represent each payment method as a subclass of the Payment class, but we want to ensure that no new payment method is added later without being handled in the code.
This is where sealed classes and interfaces come in handy. We can define a sealed class PaymentMethod, and define all the payment methods as subclasses of it. This ensures that all possible payment methods are known and defined in one place, and the code handling the payment methods can be written using a when expression to cover all possible cases.
Here's an example of how we can use sealed classes and interfaces to define payment methods:
sealed class PaymentMethod {
? ? abstract fun processPayment(): Boolean
? ??
? ? class CreditCard(val cardNumber: String, val cvv: Int) : PaymentMethod() {
? ? ? ? override fun processPayment(): Boolean {
? ? ? ? ? ? println("Processing the payment using credit card")
? ? ? ? ? ? // Process the payment for the credit card
? ? ? ? ? ? return true
? ? ? ? }
? ? }
? ??
? ? class DebitCard(val cardNumber: String, val cvv: Int, val pin: String) : PaymentMethod() {
? ? ? ? override fun processPayment(): Boolean {
? ? ? ? ? ? println("Processing the payment using debit card")
? ? ? ? ? ? // Process the payment for the debit card
? ? ? ? ? ? return true
? ? ? ? }
? ? }
? ??
? ? class PayPal(val email: String, val password: String) : PaymentMethod() {
? ? ? ? override fun processPayment(): Boolean {
? ? ? ? ? ? // Process the payment using PayPal
? ? ? ? ? ? println("Processing the payment using PayPal")
? ? ? ? ? ? return true
? ? ? ? }
? ? }
}
fun processPayment(paymentMethod: PaymentMethod) {
? ? when(paymentMethod) {
? ? ? ? is PaymentMethod.CreditCard -> paymentMethod.processPayment()
? ? ? ? is PaymentMethod.DebitCard -> paymentMethod.processPayment()
? ? ? ? is PaymentMethod.PayPal -> paymentMethod.processPayment()
? ? }
}
fun main() {
? ? // Usage
? ? val creditCard = PaymentMethod.CreditCard("1234567890123456", 123)
? ? processPayment(creditCard)
}
/*
Op =>
Processing the payment using credit card
*/
In the above example, we defined a sealed class PaymentMethod, which is the parent class for different payment methods. We defined three payment methods, CreditCard, DebitCard, and PayPal, as subclasses of PaymentMethod. Each payment method overrides the processPayment() function, which is used to process the payment for that particular payment method.
We then defined a function processPayment() that takes a PaymentMethod object as an argument and uses a when expression to handle each payment method's case. This ensures that all possible payment methods are handled, and no new payment method can be added without being handled in the code.
In conclusion, sealed classes and interfaces are a powerful feature of the Kotlin language that helps in creating more robust and maintainable code. It ensures that all possible subclasses of a parent class or interface are known and defined in one place and allows creating exhaustive expressions that handle all possible cases.
Enum Class
In Kotlin, Enum classes are a special type of class that can be used to define a fixed set of constants. Each constant in an enum class represents a single value of the enum type. Enums can be used to define things like colors, days of the week, or any other set of related constants.
Syntax:
Enum classes are declared using the keyword 'enum' followed by the class name and the list of constants in parentheses. Here is an example:
enum class Color {
RED,
GREEN,
BLUE
}
Advantages:
- Enum classes provide a type-safe way to define a fixed set of constants.
- Enum constants are instances of the enum class, which means they can have properties and methods just like any other class instance.
- Enums can be used in switch statements and when expressions to make code more readable and concise.
- Enums can be used as arguments to functions, which can help prevent errors by restricting the possible values that can be passed.
Disadvantages:
- Enum classes can become overly complex if there are too many constants or if the constants have complex properties or methods.
- Enums are not extensible, which means that you cannot add new constants to an existing enum class at runtime. This can make it difficult to change the behavior of an enum class once it has been defined.
Suitable scenarios for using Enum Class:
- When defining a set of constants.
- When modeling a limited set of related values.
- When the values can be used interchangeably with each other.
Example:
Here is an example of an enum class that represents the days of the week:
enum class DayOfWeek {
? ? MONDAY,
? ? TUESDAY,
? ? WEDNESDAY,
? ? THURSDAY,
? ? FRIDAY,
? ? SATURDAY,
? ? SUNDAY
}
fun main() {
? ? val day = DayOfWeek.MONDAY
? ??
? ? when (day) {
? ? ? ? DayOfWeek.MONDAY -> println("It's Monday!")
? ? ? ? DayOfWeek.TUESDAY -> println("It's Tuesday!")
? ? ? ? DayOfWeek.WEDNESDAY -> println("It's Wednesday!")
? ? ? ? DayOfWeek.THURSDAY -> println("It's Thursday!")
? ? ? ? DayOfWeek.FRIDAY -> println("It's Friday!")
? ? ? ? DayOfWeek.SATURDAY -> println("It's Saturday!")
? ? ? ? DayOfWeek.SUNDAY -> println("It's Sunday!")
? ? }
}
In this example, we declare an enum class called DayOfWeek that contains the seven days of the week as constants. We then create a variable called day and assign it the value DayOfWeek.MONDAY. Finally, we use a when expression to print a message depending on the value of day.
By Conclusion - Enum classes are a powerful tool for defining a fixed set of constants in Kotlin. They provide a type-safe and readable way to represent related values in your code. By using enums, you can make your code more concise and easier to understand. However, it's important to remember that enums are not extensible and can become overly complex if used improperly.
Similarity of enum class with sealed class:
- Both are used to define a limited or restricted set of values that a variable can take.
- Both enum class and sealed class can be used in when expressions to handle different cases.
- Both enum class and sealed class can have properties and methods associated with them.
- Both enum class and sealed class are used to model hierarchies of related data.
Difference b/w enum class with sealed class:
- Usage: Enums are typically used to represent a fixed number of values (e.g. days of the week, colors, etc.) whereas sealed classes are used to represent a fixed hierarchy of types with shared behavior.
- Constructors: Enum classes can have properties and methods, but their constructors cannot take any parameters other than the enum constants themselves. Sealed classes can have constructors that take parameters, allowing for more flexibility in their use.
- Instances: Each instance of an enum class is a separate object, whereas each subclass of a sealed class is a separate class.
- Comparison: Enum constants can be compared using the "==" operator, whereas sealed class instances cannot be compared in this way. Instead, sealed class instances must be compared using the "is" operator.
- Extensibility: Enum classes are not extensible because enum class are always final, whereas sealed classes are extendable. This means that you cannot create new instances of an enum class at runtime, but you can create new subclasses of a sealed class.
- Type Hierarchy: Enum classes have a fixed set of values with no common supertype, whereas sealed classes have a fixed set of subclasses with a common supertype. This means that you can use a sealed class as a type parameter, but you cannot do so with an enum class.
- Object Identity: Each value of an enum class is a separate object with its own identity, whereas a subclass of a sealed class is a separate object but has the same identity as other instances of the same subclass.
Overall, enum classes are useful for representing a fixed set of values, while sealed classes are more flexible and allow for the creation of new subclasses with shared behavior.
// Enum class
enum class Color {
? ? RED,
? ? BLUE,
? ? GREEN
}
// Sealed class
sealed class Result {
? ? class Success(val value: Int) : Result()
? ? class Failure(val error: String) : Result()
}
// Using enum class
fun printColor(color: Color) {
? ? when (color) {
? ? ? ? Color.RED -> println("Red")
? ? ? ? Color.BLUE -> println("Blue")
? ? ? ? Color.GREEN -> println("Green")
? ? }
}
// Using sealed class
fun handleResult(result: Result) {
? ? when (result) {
? ? ? ? is Result.Success -> println("Success: ${result.value}")
? ? ? ? is Result.Failure -> println("Failure: ${result.error}")
? ? ? ? is PartialSuccess -> println("Partial Success: ${result.value}")
? ? }
}
// Cannot add new value to enum class at runtime
// val color = Color.YELLOW?
// Can add new subclass to sealed class at runtime
class PartialSuccess(val value: Int) : Result()
fun main() {
? ? printColor(Color.RED) // prints "Red"
? ? handleResult(Result.Success(42)) // prints "Success: 42"
? ? handleResult(Result.Failure("Error")) // prints "Failure: Error"
? ??
? ? val partialSuccess = PartialSuccess(10)
? ? handleResult(partialSuccess) // prints nothing since PartialSuccess is not handled in the when expression
}
/*
Op =>
Red
Success: 42
Failure: Error
Partial Success: 10
*/
In the above example, we define an enum class Color and a sealed class Result. We then use these classes to define functions that take parameters of these types. The printColor function takes a Color parameter and prints the corresponding color. The handleResult function takes a Result parameter and handles the possible subclasses.
We also demonstrate the differences between the two classes. We cannot add a new value to the Color enum class at runtime, but we can add a new subclass to the Result sealed class at runtime. We also see that each value of the Color enum class has its own identity, whereas instances of the subclasses of the Result sealed class have the same identity as other instances of the same subclass.
Sealed classes, sealed interfaces, and enum classes are powerful language features in Kotlin that enable developers to create expressive and concise code. They provide a mechanism for defining a restricted set of classes or interfaces, which can help ensure type safety and facilitate pattern matching in functional programming. Sealed classes allow developers to define a hierarchy of classes with a fixed number of subclasses, while sealed interfaces restrict the number of implementing classes. Enum classes provide a convenient way to define a set of related constants, with each constant having its own value and behavior. However, it's important to note that each of these constructs has its own advantages and disadvantages, and the choice of which to use will depend on the specific requirements of a given project.
Thanks for reding till end .Please comment if any !.