Kotlin: Kotlin数据类与密封类详解
最后更新:2026-08-26
是 Kotlin 消灭样板代码的杀手锏——1 行代码自动生成 equals/hashCode/toString/copy/componentN。 则用有限的类型层次让编译器强制穷尽检查,Charlie 用它建模订单状态机零遗漏。
1. 你将学到
- 自动生成的 6 个方法
- 解构声明:
- 穷尽 when 分支
- 密封类建模状态机
- Charlie 实战: +
2. 一个开发者的真实故事
(1) 痛点:手写 equals 导致 Bug
Bob 在 Java 的 Order 类手写了 equals 方法,漏比较了 status 字段。结果 Set 去重时把不同状态的订单当作同一订单,导致 200 笔重复处理。
(2) data class 自动生成的解法
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
编译器自动生成基于所有构造器属性的 equals/hashCode,不可能漏字段。
3. data class 自动生成
(1) 基本用法
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 个方法
| 方法 | 功能 | 基于 |
|---|---|---|
| 值相等判断 | 所有构造器属性 | |
| 哈希计算 | 所有构造器属性 | |
| 字符串表示 | ||
| 创建修改副本 | 指定属性新值 | |
| 解构声明 | 按属性声明顺序 | |
| 解构声明 | 属性 N 对应 componentN |
(3) copy 深度拷贝注意
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())
⚠️ 注意: 是浅拷贝,引用类型属性仍然共享。需要深拷贝必须手动处理。
4. 解构声明
(1) 基本解构
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) 解构应用场景
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) 密封类定义有限类型层次
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) 穷尽 when 分支
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) 密封类状态机
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
| 维度 | |||
|---|---|---|---|
| 子类数量 | 固定枚举值 | 有限(编译期已知) | 无限(运行时可扩展) |
| 子类可持状态 | 单例 | 各自独立状态 | 各自独立状态 |
| when 穷尽 | ✅ | ✅ | ❌(需 else) |
| 子类类型 | 同一类 | 不同类 | 不同类 |
| 扩展性 | 不可扩展 | 有限扩展 | 完全开放 |
| 适用场景 | 简单枚举 | 有限类型+状态 | 开放继承 |
6. Charlie 实战:订单事件溯源
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. 完整示例:OrderProcessor 状态机
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") }
}
输出:
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
❓ 常见问题
Q data class 可以继承其他类吗?
A 可以继承非 data 类,但不能继承另一个 data 类。data class 可以实现接口。
Q data class 的 copy 是深拷贝吗?
A 不是。copy 是浅拷贝,引用类型属性仍共享同一对象。需要深拷贝必须手动处理。
Q sealed class 的子类必须在同一文件吗?
A Kotlin 1.5 之前必须在同一文件;1.5+ 只需在同一包内。这给了更大的组织灵活性。
Q sealed interface 和 sealed class 有什么区别?
A sealed interface(Kotlin 1.5+)允许类多实现,而 sealed class 只能单继承。sealed interface 更灵活。
Q 为什么密封类可以省略 when 的 else?
A 编译器知道密封类的所有子类,因此能验证 when 是否覆盖了所有情况。新增子类时编译器会报错,提醒你补充分支。
Q data class 的主构造器必须全是 val/var 吗?
A 不是,但只有 val/var 的属性才参与 equals/hashCode/copy/toString。普通参数不参与。
📖 小节
- 自动生成 equals/hashCode/toString/copy/componentN,1 行替代 60+ 行
- 是浅拷贝,引用类型属性仍共享
- 解构声明 基于自动生成的 componentN
- 建立有限类型层次,编译器强制 when 穷尽检查
- 密封类适合建模状态机、事件类型、结果类型
- sealed class > enum:子类可各自持有不同状态数据
📝 作业
- 基础题(难度⭐):用 定义 ,测试 、 和解构声明。提示:
- 进阶题(难度⭐⭐):用 建模成功/失败,编写 处理两种情况。提示:
- 挑战题(难度⭐⭐⭐):用密封类建模完整的订单状态机(Pending→Processing→Shipped→Delivered / Cancelled),实现 函数,确保非法转换抛异常。提示: 穷尽检查 +