Kotlin Sealed Classes vs. Data Classes: What's the Difference?
Sealed classes and data classes are powerful Kotlin features—but what’s the difference? In this post, we’ll explore their key distinctions, practical use cases, and how combining them can improve your Kotlin code.
When you're exploring Kotlin, two powerful language features you'll often encounter are sealed classes and data classes. While both simplify your code and enhance readability, they serve distinctly different purposes. Let's dive into the key differences between them and discover when to use each one effectively.
Data Classes: Your Data-Holding Companion
Data classes in Kotlin are specifically designed to hold data. They eliminate boilerplate code by automatically generating essential methods like equals()
, hashCode()
, toString()
, and even a convenient copy()
function.
Here's a simple example:
data class User(val name: String, val age: Int)
val alice = User("Alice", 30)
val olderAlice = alice.copy(age = 31)
Key Features of Data Classes:
- Automatic method generation: equals(), hashCode(), toString(), copy(), componentN()
- Immutable by default: Encourages safer, predictable code
- Ideal for DTOs and simple data containers
Sealed Classes: Restricting Your Class Hierarchies
Sealed classes, on the other hand, allow you to define a restricted set of subclasses. This makes them perfect for representing finite, well-defined hierarchies or states within your application.
Consider this example of handling API responses:
sealed class ApiResponse {
data class Success(val data: String) : ApiResponse()
data class Error(val exception: Throwable) : ApiResponse()
}
fun handleResponse(response: ApiResponse) {
when (response) {
is ApiResponse.Success -> println("Success! Data: ${response.data}")
is ApiResponse.Error -> println("Error occurred: ${response.exception.message}")
}
}
Notice how the compiler ensures you've handled all possible cases—no need for an else
block!
Key Features of Sealed Classes:
- Limited subclassing: All subclasses must be declared in the same file or package
- Compile-time safety: Exhaustive checks in
when
expressions - Great for state management and type-safe hierarchies
Comparing Data and Sealed Classes at a Glance
Feature | Data Class | Sealed Class |
---|---|---|
Purpose | Hold simple data | Restrict class hierarchies |
Inheritance | Cannot be extended | Intended to be subclassed |
Generated methods | equals(), hashCode(), copy(), toString() | None generated automatically |
Instantiation | Directly instantiated | Abstract; cannot instantiate directly |
Ideal Use Cases | DTOs, POJOs | States, results, restricted hierarchies |
Can They Work Together?
Absolutely! In fact, combining sealed and data classes is a powerful Kotlin idiom. You can define your sealed hierarchy clearly while leveraging the convenience of data classes for each subtype:
sealed class Result {
data class Success(val value: Int) : Result()
data class Failure(val reason: String) : Result()
}
This approach gives you the best of both worlds—concise syntax, automatic method generation, and exhaustive type checking.
When Should You Use Each?
Choose Data Classes When:
- You're modeling straightforward data objects.
- You want automatic implementations of common methods.
- Your objects don't require inheritance or polymorphism.
Choose Sealed Classes When:
- You have a clearly defined set of related types or states.
- You want compile-time checking for exhaustive handling.
- You're modeling complex states or hierarchical structures.
Understanding these differences empowers you to write cleaner, safer, and more maintainable Kotlin code. Whether you're building a REST API with Spring Boot or designing robust domain models, knowing when to use sealed classes versus data classes will significantly enhance your Kotlin craftsmanship.
Happy coding!