Kotlin Data Classes and Sealed Classes: Features, Copy, Destructuring & Type-Safe Hierarchies
1. Data Classes
What are data classes in Kotlin?
Data classes are special classes designed to hold data, automatically providing implementations for common methods like toString(), equals(), hashCode(), and copy().
Syntax: Declared with the data keyword: data class ClassName(val/var property: Type).
Features:
- Automatic
toString()for readable output. - Automatic
equals()andhashCode()for comparison and hashing. copy()for creating modified copies.- Destructuring declarations via component functions (e.g.,
component1()).
Requirements: Must have at least one primary constructor parameter with val or var.
Use Case: Representing data models (e.g., user profiles, API responses).
Limitation: Cannot be abstract, open, sealed, or inner.
Example:
// Data class example
data class User(val id: Int, val name: String, val email: String)
fun main() {
// Create instances
val user1 = User(1, "Krishna", "[email protected]")
val user2 = User(1, "Krishna", "[email protected]")
val user3 = user1.copy(email = "[email protected]")
// toString()
println("User1: $user1")
// equals()
println("User1 == User2: ${user1 == user2}")
// copy()
println("User3 (copied): $user3")
// Destructuring
val (id, name, email) = user1
println("Destructured: id=$id, name=$name, email=$email")
}
User1: User(id=1, name=Krishna, [email protected])
User1 == User2: true
User3 (copied): User(id=1, name=Krishna, [email protected])
Destructured: id=1, name=Krishna, [email protected]
Note: User auto-generates toString(), equals(), hashCode(), and copy(). copy() creates a new instance with modified properties. Destructuring allows accessing properties via componentN() functions.
2. Sealed Classes
What are sealed classes in Kotlin?
Sealed classes are a restricted class hierarchy where all subclasses must be defined within the same file or module (in Kotlin 1.5+).
Syntax: Declared with the sealed keyword: sealed class ClassName.
Features:
- Restricts inheritance to a fixed set of subclasses.
- Ideal for representing restricted hierarchies (e.g., state machines, API responses).
- Enhances type safety with
whenexpressions, as the compiler ensures all cases are handled.
Subclasses: Can be data class, object, or regular class within the sealed hierarchy.
Use Case: Modeling finite states (e.g., success/error/loading, algebraic data types).
Limitation: Subclasses must be in the same file (pre-Kotlin 1.5) or module (Kotlin 1.5+).
Example:
// Sealed class example
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()
}
fun processResult(result: Result): String {
return when (result) {
is Result.Success -> "Success: ${result.data}"
is Result.Error -> "Error: ${result.message}"
is Result.Loading -> "Loading..."
}
}
fun main() {
val success = Result.Success("Data loaded")
val error = Result.Error("Failed to load")
val loading = Result.Loading
println(processResult(success))
println(processResult(error))
println(processResult(loading))
}
Success: Data loaded
Error: Failed to load
Loading...
Note: Result is a sealed class with three subclasses: Success, Error, and Loading. when expression ensures all cases are handled, leveraging type safety. Subclasses can be data classes (Success, Error) or objects (Loading).
3. Comprehensive Example Combining Data Classes and Sealed Classes
Example:
// Comprehensive example with data classes and sealed classes
data class User(val id: Int, val name: String, val email: String)
sealed class UserOperation {
data class Created(val user: User) : UserOperation()
data class Updated(val user: User) : UserOperation()
data class Deleted(val userId: Int) : UserOperation()
object Fetching : UserOperation()
}
fun processOperation(operation: UserOperation): String {
return when (operation) {
is UserOperation.Created -> {
val (id, name, email) = operation.user
"Created user: id=$id, name=$name, email=$email"
}
is UserOperation.Updated -> "Updated user: ${operation.user}"
is UserOperation.Deleted -> "Deleted user with id: ${operation.userId}"
is UserOperation.Fetching -> "Fetching user..."
}
}
fun main() {
// Create data class instances
val user1 = User(1, "Krishna", "[email protected]")
val user2 = user1.copy(id = 2, name = "Ram")
// Create sealed class instances
val operations = listOf(
UserOperation.Created(user1),
UserOperation.Updated(user2),
UserOperation.Deleted(1),
UserOperation.Fetching
)
// Process operations
operations.forEach { operation ->
println(processOperation(operation))
}
// Demonstrate equality and copy
println("\nUser1 == User2: ${user1 == user2}")
println("User1 copy: ${user1.copy(email = "[email protected]")}")
}
Created user: id=1, name=Krishna, [email protected]
Updated user: User(id=2, name=Ram, [email protected])
Deleted user with id: 1
Fetching user...
User1 == User2: false
User1 copy: User(id=1, name=Krishna, [email protected])
Description: Data Class: User provides toString(), equals(), copy(), and destructuring. Sealed Class: UserOperation models user-related operations with subclasses. Integration: when processes UserOperation instances, leveraging User properties. Demonstrates type safety and concise data handling.
4. Common Mistakes and Best Practices
Common Mistakes:
- Data Classes:
- Including non-data properties (e.g., computed properties) in the primary constructor, affecting
equals()/hashCode(). - Overusing data classes for complex logic instead of regular classes.
- Forgetting
val/varin primary constructor, causing compilation errors.
- Including non-data properties (e.g., computed properties) in the primary constructor, affecting
- Sealed Classes:
- Not handling all subclasses in
whenexpressions, risking incomplete logic. - Defining subclasses outside the file/module (pre-Kotlin 1.5), causing errors.
- Overcomplicating hierarchies, reducing readability.
- Not handling all subclasses in
- General:
- Not leveraging Kotlin's null safety with data/sealed classes.
- Ignoring immutability (
val) for data class properties when possible.
Best Practices:
- Data Classes:
- Use for simple data holders with minimal logic.
- Prefer
valfor immutable properties to ensure thread safety. - Use
copy()for creating modified instances instead of manual updates. - Document properties with KDoc for clarity.
- Sealed Classes:
- Use for restricted hierarchies with clear state representations.
- Ensure
whenexpressions cover all subclasses for type safety. - Keep subclasses concise, using data classes or objects where appropriate.
- General:
- Leverage Kotlin's null safety (e.g.,
String?for nullable fields). - Follow Kotlin coding conventions: PascalCase for classes, camelCase for properties.
- Test edge cases (e.g., null values, empty data classes).
- Use IDE tools (e.g., IntelliJ IDEA) to catch errors in sealed class hierarchies.
- Leverage Kotlin's null safety (e.g.,