Sitemap

Sealed Class

9 min readNov 27, 2024

--

Kotlin Sealed Class

A sealed class in Kotlin is a special type of class used to represent a restricted hierarchy of types. It allows you to define a class and its possible subclasses in the same file, ensuring that no other subclasses can be created elsewhere.

A sealed class in Kotlin restricts the class hierarchy by ensuring that all subclasses must be declared in the same file as the sealed class. This restriction is enforced by the compiler, preventing other files or modules from adding subclasses. The main purpose is to keep the hierarchy controlled and predictable.

//Creating the Sealed Class
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val error: String) : Result()
object Loading : Result()
}



// Sending or assigning the value to Sealed class type / sub class
val resultLoading: Result = Result.Loading
handleResult(resultLoading)

val resultSuccess: Result = Result.Success("Success")
handleResult(resultSuccess)

val resultError: Result = Result.Error("Error")
handleResult(resultError)




// Handling the result or sub type of sealed class data
fun handleResult(result: Result) {
when (result) {
is Result.Success -> println("Data: ${result.data}")
is Result.Error -> println("Error: ${result.error}")
// Result.Loading -> println("Loading...")
// No `else` branch needed as all subclasses are handled
else -> {
println("Unknown result")
}
}
}

How it working behand the seen or hood?

Kotlin compiles sealed classes into an abstract class in Java. The subclasses (Success, Error, and Loading) are defined as nested static classes. Here's how your sealed class might look after compilation:

public abstract class Result {
private Result() {} // Private constructor to restrict subclassing

// Static nested class for Success
public static final class Success extends Result {
private final String data;

public Success(String data) {
this.data = data;
}

public String getData() {
return data;
}

@Override
public String toString() {
return "Success(data=" + data + ")";
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Success success = (Success) o;
return data.equals(success.data);
}

@Override
public int hashCode() {
return data.hashCode();
}
}

// Static nested class for Error
public static final class Error extends Result {
private final String error;

public Error(String error) {
this.error = error;
}

public String getError() {
return error;
}

@Override
public String toString() {
return "Error(error=" + error + ")";
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Error error1 = (Error) o;
return error.equals(error1.error);
}

@Override
public int hashCode() {
return error.hashCode();
}
}

// Static nested object for Loading
public static final class Loading extends Result {
public static final Loading INSTANCE = new Loading();

private Loading() {}

@Override
public String toString() {
return "Loading";
}
}
}

2. Key Points About How It Works

  1. Abstract Parent Class:
  • Result is compiled as an abstract class. It cannot be instantiated directly because of the private constructor (private Result()).
  • This ensures that only the subclasses defined in the same file can inherit from Result.

2. Static Subclasses:

  • Each subclass (Success, Error, and Loading) is compiled as a static nested class.
  • This avoids creating an unnecessary reference to the outer class, improving performance.

3. Singleton for object:

  • The Loading subclass is compiled as a singleton instance with a private constructor and a public static INSTANCE field.

4. Data Classes for Success and Error:

  • The Success and Error classes are compiled as static final classes with:
  • Private fields for their data (data and error).
  • Auto-generated equals, hashCode, and toString methods (thanks to the Kotlin data keyword).

5. Type Safety:

  • The sealed modifier ensures the compiler knows all possible subclasses of Result.
  • This enables exhaustive when expressions at compile time, as the compiler verifies that all cases are handled.

Key Features of Sealed Classes:

  1. Restricted Inheritance or Hierarchy:
  • The compiler checks that all subclasses of the sealed class are declared in the same file.
  • If you try to subclass a sealed class in another file, the compiler throws an error.
  • Subclasses of a sealed class must be defined in the same file as the sealed class itself.
  • The compiler keeps track of all subclasses of a sealed class during compilation.
// File 1
sealed class Result{
// some subtype of Result
}


// File 2
class CustomResult : Result() // Error: Cannot inherit from sealed class

2. Exhaustive Checking:

  • The compiler enforces exhaustive handling of all possible subclasses of a sealed class in when expressions. If you miss a subclass, the compiler gives an error or warning.
  • Kotlin knows all possible subclasses and can enforce exhaustive handling of all cases.
// Handling the result or sub type of sealed class data
fun handleResult(result: Result) {
when (result) {
is Result.Success -> println("Data: ${result.data}")
is Result.Error -> println("Error: ${result.error}")
// Result.Loading -> println("Loading...")
else -> {
println("Unknown result")
}
// No `else` branch needed as all subclasses are handled

// if u not handle all subclass or else block then Compiler error if Result.Error is not handled
}
}

3. Useful for Representing State:

  • Sealed classes are often used for modeling states or results, such as in state management or handling UI states.

Important Point and Limitation of Sealed class

1. Restricted Class Hierarchy:

  • All subclasses must be defined in the same file as the sealed class. This ensures tight control over the type hierarchy and avoids unexpected subclassing.
  • All subclasses must be declared in the same file. While this enhances control, it can lead to clutter in large files.
  • You cannot add new subclasses in other files or modules. This makes sealed classes unsuitable for use cases where you want extensibility.

2. Exhaustive Handling in when Expressions

  • The compiler enforces that all possible subclasses of a sealed class are handled in a when expression. This avoids runtime errors due to unhandled cases.

3. Inherits From Any:

  • A sealed class is an abstract class, so it can have common functionality or properties that all subclasses inherit.
  • A sealed class is abstract and cannot be instantiated directly. You must use one of its subclasses.

4. Extending a Class(Abstract or Normal)

  • A sealed class can extend an abstract or any suber class. Any abstract members in the parent class must be implemented by the sealed class or its subclasse

5. Implementing an Interface

  • A sealed class can implement one or more interface. All its subclasses must also conform to the interface’s contract by implementing its members.
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val error: String) : Result()
object Loading : Result()
}

fun main() {
val resultLoading: Result = Result.Loading
handleResult(resultLoading)

val resultSuccess: Result = Result.Success("Success")
handleResult(resultSuccess)

val resultError: Result = Result.Error("Error")
handleResult(resultError)



}
fun handleResult(result: Result) {
when (result) {
is Result.Success -> {
println("Success: ${result.data}")
}
is Result.Error -> {
println("Error: ${result.error}")
}
is Result.Loading -> {
println("Loading")
}
else -> {
println("Unknown result")

}
}
}

//Sealed class can be extended others class:
sealed class Result2: MyClass1() {

}

//Sealed class can be extended others sealed class but in same file
sealed class Result3: Result2() {

}

//Sealed class can Extent the Abstract class but they need to implement abstract method
sealed class Result4: MyAbstractClass() {
/* override fun doSomething() {
TODO("Not yet implemented")
}*/

data class Success(val data: String) : Result4() {
override fun doSomething() {
TODO("Not yet implemented")
}
}
}



//Sealed class can implement the Interface but they to implement all Abstract method
sealed class Result5: MyInterface {
/* override fun doSomething2() {
TODO("Not yet implemented")
}
*/
data class Success(val data: String) : Result5() {
override fun doSomething2() {
TODO("Not yet implemented")
}
}
}





//We can create subclass of sealed class in same file but not outside the file.
class subSealed: Result(){
fun subSealedFun(){
println("subSealedFun")
}
}


open class MyClass1 {
constructor (): super()
}

abstract class MyAbstractClass {
public abstract fun doSomething()
}

interface MyInterface {
public abstract fun doSomething2()
}

Frequent Interview Questions

1. What is a sealed class in Kotlin, and why is it used?

A sealed class is a special kind of class in Kotlin used to represent a restricted hierarchy of types. All subclasses of a sealed class must be declared in the same file as the sealed class itself. This ensures that the hierarchy is closed, and the compiler knows all possible subclasses at compile time.

Get Abhishek Rai’s stories in your inbox

Join Medium for free to get updates from this writer.

Key Use Cases:

  • Modeling states or results (e.g., Success, Error, Loading).
  • Ensuring exhaustive when expressions without needing a default case.
  • Providing better safety and clarity when working with fixed sets of types.

2. How does a sealed class differ from an abstract class in Kotlin?

  • Restricted hierarchy: A sealed class restricts subclasses to the same file, while an abstract class can have subclasses in any file or module.
  • Exhaustive handling: Sealed classes allow the compiler to enforce exhaustive handling in when expressions, which is not possible with abstract classes.
  • Purpose: Sealed classes are ideal for representing fixed sets of types, while abstract classes are used for general-purpose inheritance.

3. Can a sealed class have methods or properties?

Yes, a sealed class can have methods and properties like any other class. Subclasses can inherit these properties and methods.

4. What are the key advantages of using sealed classes?

  • Ensures type safety by restricting the hierarchy.
  • Enables exhaustive when handling, reducing runtime errors.
  • Provides a clear and concise way to model state machines or fixed domains.
  • Enhances code readability and maintainability by grouping related types.

5. Can sealed classes be extended in other files or modules?

No, all subclasses of a sealed class must be declared in the same file as the sealed class. This restriction ensures the hierarchy is closed and known to the compiler.

6. How does the compiler behave when a when expression for a sealed class is not exhaustive?

The compiler will give a warning or error, prompting you to handle all possible subclasses in the when expression.

7. What is the difference between sealed classes and enums in Kotlin?

Press enter or click to view image in full size

8. Can a sealed class have an object as a subclass? What’s its purpose?

Yes, a sealed class can have an object as a subclass. The object is used when you want a singleton representation of a type (e.g., a single Loading state).

Example:

sealed class State {
object Loading : State()
data class Success(val data: String) : State()
data class Error(val message: String) : State()
}

9. Can a sealed class have private constructors?

Yes, sealed classes can have private constructors, but it is unnecessary since the sealed keyword itself restricts subclassing to the same file. Adding a private constructor is redundant.

10. What are the limitations of sealed classes?

  • All subclasses must be declared in the same file, which can lead to clutter in large files.
  • Cannot support deeply nested hierarchies.
  • Not suitable for open systems where new types need to be added frequently.
  • Limited to a flat hierarchy; indirect inheritance is not allowed.

11. How does sealed class improve state management in Android?

Sealed classes are commonly used in Android to represent UI states in a ViewModel. Each state (e.g., Loading, Success, Error) is modeled as a subclass, ensuring predictable and type-safe handling in the UI layer.

12. How are sealed classes implemented under the hood?

Kotlin compiles sealed classes into abstract classes with a private constructor. This prevents direct instantiation and ensures that all subclasses must be defined in the same file.

sealed class Result
data class Success(val data: String) : Result()
object Failure : Result()
public abstract class Result {
private Result() {}
public static final class Success extends Result { /*...*/ }
public static final class Failure extends Result { /*...*/ }
}

13. Can sealed classes be generic? What are the implications?

Yes, sealed classes can be generic. This allows for more flexibility in representing types.

sealed class ApiResponse<T> {
data class Success<T>(val data: T) : ApiResponse<T>()
data class Failure<T>(val error: String) : ApiResponse<T>()
}

fun <T> handleResponse(response: ApiResponse<T>) = when (response) {
is ApiResponse.Success -> println("Data: ${response.data}")
is ApiResponse.Failure -> println("Error: ${response.error}")
}

14. Can sealed classes be nested? What are the use cases?

Yes, sealed classes can be nested within another class. This is useful for scoping related states or results.

class NetworkOperations {
sealed class NetworkState {
object Loading : NetworkState()
data class Success(val data: String) : NetworkState()
data class Error(val error: String) : NetworkState()
}
}

fun handleNetworkState(state: NetworkOperations.NetworkState) {
when (state) {
is NetworkOperations.NetworkState.Loading -> println("Loading...")
is NetworkOperations.NetworkState.Success -> println("Data: ${state.data}")
is NetworkOperations.NetworkState.Error -> println("Error: ${state.error}")
}
}

--

--