404 Not Found

404 Not Found


nginx

Kotlin Data Classes and Sealed Classes Explained

data class is Kotlin's killer feature for eliminating boilerplate — 1 line auto-generates equals/hashCode/toString/copy/componentN. sealed class builds finite type hierarchies that let the compiler enforce exhaustiveness — Charlie uses it to model the order state machine with zero missed cases.

1. What You'll Learn


2. A Developer's Real Story

(1) Pain Point: Hand-Written equals Causing Bugs

Bob hand-wrote an equals method for the Java Order class but forgot to compare the status field. As a result, the Set deduplication treated orders with different statuses as the same order, causing 200 duplicate processing operations.

(2) data class Auto-Generated Solution

KOTLIN
// Java: 30 lines of error-prone equals/hashCode
// Kotlin: 1 line, compiler-generated and correct
data class Order(val id: String, val total: Double, val status: String)

// equals compares ALL constructor properties - can't forget a field

data class compiler auto-generates equals/hashCode based on all constructor properties — impossible to miss a field.


3. data class Auto-Generation

(1) Basic Usage

KOTLIN
data class Order(val id: String, val total: Double, val status: String)

val o1 = Order("ORD-001", 299.99, "CONFIRMED")
val o2 = Order("ORD-001", 299.99, "CONFIRMED")

// Auto-generated equals: compares all constructor properties
println(o1 == o2)  // true

// Auto-generated toString
println(o1)  // Order(id=ORD-001, total=299.99, status=CONFIRMED)

// Auto-generated copy: create modified instance
val updated = o1.copy(status = "SHIPPED")
println(updated)  // Order(id=ORD-001, total=299.99, status=SHIPPED)

// Auto-generated hashCode
println(o1.hashCode() == o2.hashCode())  // true

(2) 6 Auto-Generated Methods

Method Purpose Based On
equals() Value equality check All constructor properties
hashCode() Hash computation All constructor properties
toString() String representation Order(id=..., total=..., status=...)
copy() Create modified copy Specified property new values
component1() Destructuring declaration By property declaration order
componentN() Destructuring declaration Property N → componentN

(3) copy Deep Copy Warning

KOTLIN
data class Order(val id: String, val items: MutableList<String>)

val o1 = Order("ORD-001", mutableListOf("Widget", "Gadget"))
val o2 = o1.copy()  // Shallow copy!

o2.items.add("Doohickey")
println(o1.items)  // [Widget, Gadget, Doohickey] - shared reference!

// Fix: explicitly copy the list
val o3 = o1.copy(items = o1.items.toMutableList())
⚠️ Note: copy() is a shallow copy — reference-type properties still share the same object. You must handle deep copying manually when needed.


4. Destructuring Declarations

(1) Basic Destructuring

KOTLIN
data class Order(val id: String, val total: Double, val status: String)

val order = Order("ORD-001", 299.99, "CONFIRMED")

// Destructuring declaration
val (id, total, status) = order
println("ID: $id, Total: \$$total USD, Status: $status")

// Partial destructuring with _
val (orderId, _, orderStatus) = order  // Skip total

// In for-loop
val orders = listOf(order, Order("ORD-002", 1_500.00, "PENDING"))
for ((oid, ototal, ostatus) in orders) {
    println("$oid: \$$ototal USD ($ostatus)")
}

(2) Destructuring Use Cases

KOTLIN
// Map entries
val statusMap = mapOf("ORD-001" to "SHIPPED", "ORD-002" to "PENDING")
for ((id, status) in statusMap) { ... }

// Function return values
data class Result(val success: Boolean, val message: String)
val (ok, msg) = processOrder()

// Lambda parameters
orders.map { (id, total) -> "Order $id: \$$total USD" }

5. sealed class

(1) Sealed Class Builds Finite Type Hierarchies

KOTLIN
// Sealed class: restricted class hierarchy
// All subclasses MUST be in the same file (Kotlin 1.5: same package)
sealed class OrderStatus {
    object Pending : OrderStatus()
    data class Processing(val step: Int, val totalSteps: Int) : OrderStatus()
    object Shipped : OrderStatus()
    data class Delivered(val deliveredAt: String) : OrderStatus()
    data class Cancelled(val reason: String) : OrderStatus()
}

(2) Exhaustive when Branches

KOTLIN
fun handleStatus(status: OrderStatus): String = when (status) {
    is OrderStatus.Pending -> "Waiting for payment"
    is OrderStatus.Processing -> "Step ${status.step}/${status.totalSteps}"
    is OrderStatus.Shipped -> "In transit"
    is OrderStatus.Delivered -> "Delivered at ${status.deliveredAt}"
    is OrderStatus.Cancelled -> "Cancelled: ${status.reason}"
    // NO else needed - compiler verifies all cases covered
}

(3) Sealed Class State Machine

100%
stateDiagram-v2
    [*] --> Pending
    Pending --> Processing : Pay
    Pending --> Cancelled : Cancel
    Processing --> Shipped : Ship
    Processing --> Cancelled : Fail
    Shipped --> Delivered : Deliver
    Delivered --> [*]
    Cancelled --> [*]

(4) sealed class vs enum class vs open class

Dimension enum class sealed class open class
Subclass count Fixed enum values Finite (known at compile time) Infinite (runtime-extensible)
Subclass state Singleton Independent state each Independent state each
when exhaustiveness ❌ (requires else)
Subclass granularity Same class Different classes Different classes
Extensibility Non-extensible Limited extension Fully open
Use case Simple enums Finite types + state Open inheritance

6. Charlie in Action: Order Event Sourcing

KOTLIN
// Domain events for event sourcing
sealed class OrderEvent {
    data class Created(val orderId: String, val total: Double, val customer: String) : OrderEvent()
    data class PaymentReceived(val orderId: String, val amount: Double) : OrderEvent()
    data class ItemAdded(val orderId: String, val sku: String, val qty: Int) : OrderEvent()
    data class Shipped(val orderId: String, val trackingCode: String) : OrderEvent()
    data class Cancelled(val orderId: String, val reason: String) : OrderEvent()
}

data class OrderState(val id: String, val total: Double, val status: String, val items: List<String>)

fun applyEvent(state: OrderState?, event: OrderEvent): OrderState = when (event) {
    is OrderEvent.Created -> OrderState(event.orderId, event.total, "PENDING", emptyList())
    is OrderEvent.PaymentReceived -> state!!.copy(status = "PAID")
    is OrderEvent.ItemAdded -> state!!.copy(items = state.items + "${event.sku}x${event.qty}")
    is OrderEvent.Shipped -> state!!.copy(status = "SHIPPED")
    is OrderEvent.Cancelled -> state!!.copy(status = "CANCELLED")
}

7. Complete Example: OrderProcessor State Machine

KOTLIN
// ============================================
// OrderProcessor - State Machine with Sealed Classes
// Feature: Order lifecycle with exhaustive when
// ============================================

sealed class OrderStatus {
    object Pending : OrderStatus()
    data class Processing(val step: Int, val totalSteps: Int) : OrderStatus()
    object Shipped : OrderStatus()
    data class Delivered(val date: String) : OrderStatus()
    data class Cancelled(val reason: String, val refundAmount: Double) : OrderStatus()
}

data class Order(val id: String, val total: Double, var status: OrderStatus = OrderStatus.Pending)

fun describeStatus(status: OrderStatus): String = when (status) {
    is OrderStatus.Pending -> "Waiting for payment"
    is OrderStatus.Processing -> "Processing step ${status.step}/${status.totalSteps}"
    is OrderStatus.Shipped -> "In transit"
    is OrderStatus.Delivered -> "Delivered on ${status.date}"
    is OrderStatus.Cancelled -> "Cancelled: ${status.reason} (refund: \$${status.refundAmount} USD)"
}

fun canTransitionTo(current: OrderStatus, next: OrderStatus): Boolean = when {
    current is OrderStatus.Pending && next is OrderStatus.Processing -> true
    current is OrderStatus.Processing && next is OrderStatus.Shipped -> true
    current is OrderStatus.Shipped && next is OrderStatus.Delivered -> true
    current is OrderStatus.Pending && next is OrderStatus.Cancelled -> true
    current is OrderStatus.Processing && next is OrderStatus.Cancelled -> true
    else -> false
}

fun main() {
    val orders = listOf(
        Order("ORD-001", 299.99, OrderStatus.Pending),
        Order("ORD-002", 1_500.00, OrderStatus.Processing(2, 5)),
        Order("ORD-003", 8_900.00, OrderStatus.Shipped),
        Order("ORD-004", 45.50, OrderStatus.Delivered("2026-01-15")),
        Order("ORD-005", 2_500.00, OrderStatus.Cancelled("Customer request", 2_500.00))
    )

    println("=== Order Status Report ===")
    orders.forEach { order ->
        println("${order.id}: ${describeStatus(order.status)} (Total: \$${order.total} USD)")
    }

    // Test transitions
    val testOrder = Order("ORD-099", 100.00)
    println("\n=== Transition Tests ===")
    println("Pending -> Processing(1,3): ${canTransitionTo(testOrder.status, OrderStatus.Processing(1, 3))}")
    println("Pending -> Shipped: ${canTransitionTo(testOrder.status, OrderStatus.Shipped)}")
    println("Pending -> Cancelled: ${canTransitionTo(testOrder.status, OrderStatus.Cancelled("Error", 100.0))}")

    // Aggregate by status type
    val statusCounts = orders.groupBy { it.status::class.simpleName }
    println("\n=== Status Distribution ===")
    statusCounts.forEach { (status, list) -> println("  $status: ${list.size} orders") }
}

Output:

TEXT
=== Order Status Report ===
ORD-001: Waiting for payment (Total: $299.99 USD)
ORD-002: Processing step 2/5 (Total: $1500.0 USD)
ORD-003: In transit (Total: $8900.0 USD)
ORD-004: Delivered on 2026-01-15 (Total: $45.5 USD)
ORD-005: Cancelled: Customer request (refund: $2500.0 USD) (Total: $2500.0 USD)

=== Transition Tests ===
Pending -> Processing(1,3): true
Pending -> Shipped: false
Pending -> Cancelled: true

=== Status Distribution ===
  Pending: 1 orders
  Processing: 1 orders
  Shipped: 1 orders
  Delivered: 1 orders
  Cancelled: 1 orders

❓ FAQ

Q Can a data class inherit from other classes?
A It can inherit from non-data classes but cannot inherit from another data class. Data classes can implement interfaces.
Q Is data class copy a deep copy?
A No. copy() is a shallow copy — reference-type properties still share the same object. Deep copying must be handled manually.
Q Must sealed class subclasses be in the same file?
A Before Kotlin 1.5, they had to be in the same file. From 1.5+, they only need to be in the same package. This allows greater organizational flexibility.
Q What's the difference between sealed interface and sealed class?
A sealed interface (Kotlin 1.5+) allows multiple implementations, while sealed class only supports single inheritance. sealed interface is more flexible.
Q Why can sealed classes omit else in when?
A The compiler knows all subclasses of a sealed class, so it can verify whether all cases are covered in when. Adding a new subclass will cause a compile error, reminding you to add the branch.
Q Must all primary constructor parameters in a data class be val/var?
A No, but only val/var properties participate in equals/hashCode/copy/toString. Regular parameters do not.

📖 Summary


📝 Exercises

  1. Beginner (⭐): Use data class to define Product and test copy(), equals(), and destructuring declarations. Hint: data class Product(val name: String, val price: Double)
  2. Intermediate (⭐⭐): Use sealed class to model Result as Success/Failure, and write handleResult to handle both cases. Hint: sealed class Result { data class Success(...) ... }
  3. Challenge (⭐⭐⭐): Model a complete order state machine using sealed classes (Pending→Processing→Shipped→Delivered / Cancelled), implement transition() that throws on illegal state transitions. Hint: when exhaustive checking + require()

← Previous | Next →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏