Kotlin: Kotlinのデータクラスとシールクラス
最終更新:2026-08-26
data classは、Kotlinの定型コードを排除するための画期的な機能です。たった1行で、equals、hashCode、toString、copy、componentNが自動生成されます。 sealed classは、コンパイラが網羅性を強制できるようにする有限の型階層を構築します。Charlieはこの機能を利用して、ケース漏れを一切許さない順序ステートマシンをモデル化しています。
1. 学習内容
data classの自動生成された 6 つのメソッド- デストラクチャリング宣言:
val (id, total) = order sealed classおよびwhenのすべてのブランチ- シールクラスによるステートマシンのモデリング
- Charlieの活躍:
data class+sealed class
2. ある開発者の実体験
(1) 課題:手書きはバグの原因となる
BobはJavaのOrderクラス用にequalsメソッドを手書きで実装しましたが、statusフィールドの比較を忘れていました。その結果、Setの重複排除処理において、ステータスの異なる注文が同一の注文として扱われ、200件の重複処理が発生しました。
(2) データクラスによる自動生成
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コンパイラは、すべてのコンストラクタのプロパティに基づいて equals/hashCode を自動生成するため、フィールドを見落とすことはあり得ません。
3. データクラスの自動生成
(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つのメソッド
| 手法 | 目的 | 根拠 |
|---|---|---|
equals() |
値の等価性チェック | すべてのコンストラクタのプロパティ |
hashCode() |
ハッシュ計算 | すべてのコンストラクタのプロパティ |
toString() |
文字列表現 | Order(id=..., total=..., status=...) |
copy() |
変更を加えたコピーを作成 | 指定されたプロパティの新しい値 |
component1() |
デストラクチャリング宣言 | プロパティ宣言順 |
componentN() |
デストラクチャリング宣言 | プロパティ N → コンポーネントN |
(3) コピー(ディープコピー)に関する警告
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())
⚠️ 注意:
copy() は浅いコピーです。参照型のプロパティは、依然として同じオブジェクトを共有しています。必要に応じて、深いコピーを手動で処理する必要があります。
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. シールクラス
(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) 分岐時の網羅性
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) シールクラス vs 列挙型クラス vs オープンクラス
| 寸法 | enum class |
sealed class |
open class |
|---|---|---|---|
| サブクラスの数 | 固定の列挙値 | 有限(コンパイル時に判明) | 無限(実行時に拡張可能) |
| サブクラスの状態 | シングルトン | それぞれ独立した状態 | それぞれ独立した状態 |
| 網羅性 | ✅ | ✅ | ❌ (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 ステートマシン
▶ サンプル: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 データクラスは他のクラスを継承できますか?
A データクラス以外のクラスを継承することはできますが、別のデータクラスを継承することはできません。データクラスはインターフェースを実装することができます。
Q データクラスのコピーはディープコピーですか?
A いいえ。
copy() はシャローコピーです。参照型のプロパティは、依然として同じオブジェクトを共有しています。ディープコピーは手動で処理する必要があります。Q シールクラスのサブクラスは同じファイルに記述する必要がありますか?
A Kotlin 1.5 以前では、同じファイルに記述する必要がありました。1.5 以降では、同じパッケージ内にあるだけでよいようになりました。これにより、構成の柔軟性が高まりました。
Q 「sealed interface」と「シールクラス」の違いは何ですか?
A 「sealed interface」(Kotlin 1.5以降)は複数の実装を許可しますが、「シールクラス」は単一継承のみをサポートします。「sealed interface」の方が柔軟性が高いです。
Q なぜ、sealed クラスでは when 節の else を省略できるのですか?
A コンパイラは sealed クラスのすべてのサブクラスを把握しているため、
when ですべてのケースが網羅されているかどうかを検証できます。新しいサブクラスを追加するとコンパイルエラーが発生し、その分岐を追加するよう促されます。Q データクラスのプライマリコンストラクタの引数はすべて val または var でなければならないか?
A いいえ。ただし、equals、hashCode、copy、toString に関与するのは val または var のプロパティのみです。通常の引数は関与しません。
📖 まとめ
data classは equals/hashCode/toString/copy/componentN を自動生成します — 1 行で 60 行以上を置き換えますcopy()は浅いコピーです。参照型のプロパティは、依然としてオブジェクトを共有しています。- デストラクチャリング宣言
val (a, b) = objは、自動生成された componentN に基づいています。 sealed classは有限型の階層を構築する — コンパイラはwhenの網羅性を保証する- シールクラスは、ステートマシン、イベント型、結果型のモデリングに最適です
sealed class>enum: サブクラスはそれぞれ異なる状態データを保持できる
📝 練習問題
- 初心者 (⭐):
data classを使ってProductを定義し、copy()、equals()、およびデストラクチャリング宣言を試してみてください。ヒント:data class Product(val name: String, val price: Double) - 中級 (⭐⭐):
sealed classを使用して、ResultをSuccess/Failureとしてモデル化し、両方のケースに対応するためにhandleResultを記述してください。 ヒント:sealed class Result { data class Success(...) ... } - 課題 (⭐⭐⭐): シールクラスを使用して、完全な注文状態マシン(保留中→処理中→発送済み→配達済み/キャンセル)をモデル化し、不正な状態遷移が発生した際に例外をスローする
transition()を実装してください。 ヒント:when網羅的なチェック +require()