Kotlin: Kotlinのコレクション操作
最終更新:2026-08-26
Kotlinのコレクション演算を使えば、Charlieは命令型のループを宣言型のパイプラインに置き換えることができます。たった1行で15行分のforループを置き換えられ、意図もより明確になります。
1. 学習内容
- 主要オペレーター:
map/filter/flatMap/groupBy/associate - レイジーシーケンス:
Sequenceは、中間コレクションの割り当てを回避する - 集計:
fold/reduce/sumOf/count - 不変 vs 可変:
ListvsMutableList - Charlieの活躍:機能的なパイプラインで数百万件の注文を処理
2. 本物の建築家の物語
(1) 課題:数百万件の注文を処理する際のメモリ不足(OOM)
CharlieはListを使用して、数百万件の注文レコードを処理しました。中間処理が行われるたびに新しいコレクションが作成され、3GBのメモリを消費した結果、OOMが発生しました。
(2) シーケンスの遅延評価による解決策
KOTLIN
// Eager: creates intermediate collections at each step
orders.map { enrich(it) } // Collection 1: 1M elements
.filter { it.total > 100 } // Collection 2: ~500K elements
.toList() // Collection 3
// Lazy: processes one element through entire pipeline
orders.asSequence()
.map { enrich(it) }
.filter { it.total > 100 }
.toList() // Only 1 final collection
シーケンスは組み立てラインのようなものです。各要素がパイプライン全体を通過してから、次の要素が処理を開始します。中間コレクションは必要ありません。
3. コア演算子
(1) map — 変換
KOTLIN
val orders = listOf(
Order("ORD-001", 299.99, "Alice"),
Order("ORD-002", 1_500.00, "Bob")
)
// Transform each element
val ids = orders.map { it.id } // [ORD-001, ORD-002]
val summaries = orders.map { "${it.id}: \$${it.total} USD" }
// mapIndexed: with index
orders.mapIndexed { i, order -> "[${i + 1}] ${order.id}" }
// mapNotNull: transform + filter nulls
val emails = orders.mapNotNull { it.customerEmail }
(2) フィルター — フィルター
KOTLIN
// Filter by predicate
val highValue = orders.filter { it.total > 1_000 }
val pending = orders.filter { it.status == "PENDING" }
// filterNot: inverse filter
val active = orders.filterNot { it.status == "CANCELLED" }
// filterIndexed: with index
orders.filterIndexed { i, _ -> i % 2 == 0 } // Even-indexed orders
(3) flatMap — 変換の平坦化
KOTLIN
data class Customer(val name: String, val orders: List<Order>)
val customers = listOf(
Customer("Alice", listOf(Order("ORD-001", 299.99), Order("ORD-002", 150.0))),
Customer("Bob", listOf(Order("ORD-003", 1_500.00)))
)
// Map + Flatten in one step
val allOrders = customers.flatMap { it.orders }
// [Order(ORD-001, 299.99), Order(ORD-002, 150.0), Order(ORD-003, 1500.0)]
(4) groupBy — グループ化
KOTLIN
// Group by key
val byCustomer: Map<String, List<Order>> = orders.groupBy { it.customer }
// Group by with value transform
val totalsByCustomer = orders.groupBy(
keySelector = { it.customer },
valueTransform = { it.total }
)
// {Alice=[299.99], Bob=[1500.0]}
(5) 関連付け — マップへの関連付け
KOTLIN
// Create map from list
val orderMap = orders.associate { it.id to it }
// {ORD-001=Order(...), ORD-002=Order(...)}
// associateBy: key selector
val byId = orders.associateBy { it.id }
// associateBy with value transform
val totalsById = orders.associateBy(
keySelector = { it.id },
valueTransform = { it.total }
)
(6) 演算子のクイックリファレンス
| 演算子 | 関数 | 入力 → 出力 | SQLでの対応 | |
|:-------|:-----|:----------|:---------|
| map | 変換 | List<A> → List<B> | 選択 |
| filter | フィルタ | List<T> → List<T> | WHERE |
| flatMap | フラット化変換 | List<A> → List<B> | JOIN + SELECT |
| groupBy | グループ | List<T> → Map<K, List<T>> | GROUP BY |
| associate | 地図 | List<T> → Map<K, V> | - |
| distinct | 重複排除 | List<T> → List<T> | DISTINCT |
| sortedBy | 並べ替え | List<T> → List<T> | ORDER BY |
4. 収集処理パイプライン
flowchart LR
A[Orders<br/>1M records] --> B[filter<br/>total > 1000]
B --> C[map<br/>extract customer]
C --> D[distinct<br/>unique customers]
D --> E[groupBy<br/>by region]
E --> F[Result<br/>Map of customers]
5. シーケンスの遅延評価
(1) イージーとレイジー
KOTLIN
// Eager (List): each step creates new collection
val result = orders
.map { println("map: ${it.id}"); it.copy(total = it.total * 0.9) }
.filter { println("filter: ${it.id}"); it.total > 100 }
.take(2)
.toList()
// Lazy (Sequence): processes one element at a time
val result2 = orders.asSequence()
.map { println("seq-map: ${it.id}"); it.copy(total = it.total * 0.9) }
.filter { println("seq-filter: ${it.id}"); it.total > 100 }
.take(2)
.toList()
// Sequence only processes elements until take(2) is satisfied
(2) シーケンスを使用するタイミング
| シナリオ | 使用リスト | 使用順序 |
|---|---|---|
| データサイズ | 10,000未満 | 10,000超 |
| 操作手順 | 1~2手順 | 3手順以上 |
| 中間結果のサイズ | ソースのサイズに近い | 大幅に縮小 |
| 複数の探索が必要 | はい | いいえ |
(3) リストとシーケンスのパフォーマンス比較
| 次元 | リスト(イージー) | シーケンス(レイジー) |
|---|---|---|
| 中間コレクション | 各ステップで作成される | 作成されない |
| メモリ | O(n × ステップ数) | O(1) |
| 短絡演算 | 最適化されていない | 最適化済み(例:take は N のみを処理する) |
| 最初の結果 | パイプラインが完全に処理されるまで待機 | すぐに利用可能 |
6. 集計演算
(1) 折りたたみ / 縮小
KOTLIN
// fold: with initial value
val totalRevenue = orders.fold(0.0) { acc, order -> acc + order.total }
// reduce: first element as initial value
val maxOrder = orders.reduce { max, order ->
if (order.total > max.total) order else max
}
// foldRight: from end to start
val reversed = orders.foldRight(emptyList<Order>()) { order, acc -> acc + order }
(2) 利便性の集約
KOTLIN
val total = orders.sumOf { it.total }
val avg = orders.map { it.total }.average()
val max = orders.maxByOrNull { it.total }
val min = orders.minByOrNull { it.total }
val count = orders.count { it.total > 1_000 }
// Sorting
val sorted = orders.sortedByDescending { it.total }
val top3 = orders.sortedByDescending { it.total }.take(3)
(3) fold と reduce の違い
| 寸法 | fold |
reduce |
|---|---|---|
| 初期値 | 必須 | 暗黙の最初の要素 |
| コレクションが空 | 安全(初期値を返す) | 例外をスローする |
| 戻り値の型 | 要素の型と異なる場合がある | 要素の型と同じ |
| 評価 | ⭐⭐⭐ | ⭐⭐ |
7. 不変コレクションと可変コレクション
(1) 読み取り専用インターフェースと変更可能なインターフェース
KOTLIN
// Read-only (immutable interface)
val list: List<Order> = listOf(Order("ORD-001", 299.99, "Alice"))
// Mutable
val mutableList: MutableList<Order> = mutableListOf()
mutableList.add(Order("ORD-002", 1_500.00, "Bob"))
// Read-only view of mutable list
val readOnly: List<Order> = mutableList // OK: MutableList extends List
// readOnly.add(...) // ERROR: List has no add method
mutableList.add(Order("ORD-003", 45.50, "Charlie")) // Changes readOnly view!
(2) 防御的なコピー
KOTLIN
class OrderProcessor(private val _orders: MutableList<Order>) {
// Defensive copy: expose immutable view
val orders: List<Order> get() = _orders.toList()
// Or use immutable view (no copy, but can be cast back)
val ordersView: List<Order> get() = _orders.toList()
}
(3) コレクションタイプの比較
| タイプ | 読み取り専用インターフェース | 変更可能なインターフェース | ファクトリ関数 |
|---|---|---|---|
| 一覧 | List |
MutableList |
listOf / mutableListOf |
| セット | Set |
MutableSet |
setOf / mutableSetOf |
| 地図 | Map |
MutableMap |
mapOf / mutableMapOf |
8. 完全な例:100万件規模の処理パイプライン
▶ サンプル:大規模データ処理パイプライン
KOTLIN
// ============================================
// OrderProcessor - Collection Pipeline
// Feature: Process orders with functional operators
// ============================================
data class Order(val id: String, val total: Double, val status: String, val customer: String, val region: String)
fun main() {
// Simulate order data
val orders = listOf(
Order("ORD-001", 299.99, "CONFIRMED", "Alice", "US"),
Order("ORD-002", 15_000.00, "CONFIRMED", "Bob", "EU"),
Order("ORD-003", 2_500.00, "PENDING", "Charlie", "US"),
Order("ORD-004", 45.50, "CANCELLED", "Alice", "ASIA"),
Order("ORD-005", 8_900.00, "CONFIRMED", "Bob", "EU"),
Order("ORD-006", 1_200.00, "SHIPPED", "Charlie", "US"),
Order("ORD-007", 350.00, "CONFIRMED", "Alice", "ASIA"),
Order("ORD-008", 22_000.00, "PENDING", "Bob", "EU"),
Order("ORD-009", 750.00, "CONFIRMED", "Charlie", "US"),
Order("ORD-010", 4_500.00, "SHIPPED", "Alice", "US")
)
// Pipeline 1: High-value confirmed orders
println("=== High-Value Confirmed Orders ===")
orders.filter { it.status == "CONFIRMED" && it.total > 1_000 }
.sortedByDescending { it.total }
.forEach { println(" ${it.id}: \$${it.total} USD (${it.customer})") }
// Pipeline 2: Revenue by region
println("\n=== Revenue by Region ===")
orders.filter { it.status != "CANCELLED" }
.groupBy { it.region }
.mapValues { (_, list) -> list.sumOf { it.total } }
.forEach { (region, revenue) -> println(" $region: \$$revenue USD") }
// Pipeline 3: Top customers by order count and revenue
println("\n=== Customer Summary ===")
orders.filter { it.status != "CANCELLED" }
.groupBy { it.customer }
.map { (customer, list) ->
val count = list.size
val total = list.sumOf { it.total }
val avg = list.map { it.total }.average()
"$customer: $count orders, \$${total} USD total, \$${"%.2f".format(avg)} avg"
}
.forEach { println(" $it") }
// Pipeline 4: Using Sequence for efficient processing
println("\n=== Top 3 Orders (Sequence) ===")
orders.asSequence()
.filter { it.status != "CANCELLED" }
.sortedByDescending { it.total }
.take(3)
.forEach { println(" ${it.id}: \$${it.total} USD") }
// Aggregate: fold to build a summary string
val summary = orders
.filter { it.status != "CANCELLED" }
.fold("Order Summary: ") { acc, order -> "$acc\n ${order.id} (\$${order.total} USD)" }
println("\n${summary}")
println("Total orders: ${orders.size}, Active: ${orders.count { it.status != "CANCELLED" }}")
}
出力:
TEXT
📖 参照専用
=== High-Value Confirmed Orders ===
ORD-005: $8900.0 USD (Bob)
ORD-002: $15000.0 USD (Bob)
=== Revenue by Region ===
US: $7599.99 USD
EU: $46400.0 USD
ASIA: $350.0 USD
=== Customer Summary ===
Alice: 2 orders, $3649.99 USD total, $1824.995 avg
Bob: 3 orders, $46400.0 USD total, $15466.666666666666 avg
Charlie: 3 orders, $4450.0 USD total, $1483.3333333333333 avg
=== Top 3 Orders (Sequence) ===
ORD-008: $22000.0 USD (Bob)
ORD-002: $15000.0 USD (Bob)
ORD-005: $8900.0 USD (Bob)
Order Summary:
ORD-001 ($299.99 USD)
ORD-002 ($15000.0 USD)
ORD-003 ($2500.0 USD)
ORD-005 ($8900.0 USD)
ORD-006 ($1200.0 USD)
ORD-007 ($350.0 USD)
ORD-008 ($22000.0 USD)
ORD-009 ($750.0 USD)
ORD-010 ($4500.0 USD)
Total orders: 10, Active: 9
❓ よくある質問
Q Kotlinのコレクション操作とJavaのStreamの違いは何ですか?
A Kotlinのコレクション演算はより簡潔であり(
itキーワード、末尾のラムダ式)、stream()/collect()への変換が不要で、SequenceはStreamに似ていますが、より軽量です。Q いつSequenceを使用すべきですか?
A データ量が多い(10,000以上)、処理ステップが多い(3つ以上)、または短絡評価(
take/first)が必要な場合にSequenceを使用してください。 データ量が少ない場合は、Listの方がシンプルです。Q
toList() はデータをコピーしますか?A はい。
toList() は新しいリストを作成します。これは「防御的コピー」です。コピーを伴わない読み取り専用のビューのみが必要な場合は、List のインターフェース参照を使用してください。Q fold と reduce — どちらを選ぶべきですか?
A
fold を推奨します。初期値を指定でき、空のコレクションに対しても安全だからです。reduce は、空のコレクションに対して例外をスローします。Q KotlinのListは本当に不変ですか?
A いいえ。Kotlinの
Listは読み取り専用インターフェース(これを通じて変更することはできません)ですが、その基盤となる実装は可変である可能性があります。 真の不変性を実現するには、toList() を使用してコピーを作成する必要があります。Q
flatMap と map + flatten には違いがありますか?A 機能的には同等ですが、
flatMap の方が効率的です(1ステップで完了します)。 これは、map + flatten を組み合わせた省略形です。📖 まとめ
- 主要な演算子:
map(変換)、filter(フィルタ)、flatMap(フラット化変換)、groupBy(グループ化)、associate(マップ) - シーケンスの遅延評価により、中間的なコレクションの割り当てを回避できる — 大規模なデータセットではこの方法が推奨される
fold(初期値あり、安全)は、reduce(空のコレクションでは安全ではない)よりも推奨されます。Listは読み取り専用インターフェースであり、MutableListは変更可能である — 読み取り専用を優先するtoList()を使用したディフェンシブコピーは、内部の可変状態を保護する- コレクション操作パイプラインにより、データ処理コードは宣言的かつ読みやすく、組み合わせやすくなります
📝 練習問題
- 初心者 (⭐):
filterとmapを使用して、注文リストから金額が 1000 USD を超える注文の ID を抽出してください。ヒント:orders.filter { }.map { } - 中級 (⭐⭐):
groupByとmapValuesを使用して、各顧客の総支出額を計算し、金額の多い順に並べ替えてください。ヒント:groupBy { it.customer }.mapValues { } - 上級 (⭐⭐⭐):
Sequenceを使用して、数百万件の注文に対する遅延処理パイプラインを実装してください:filter → map → take(100)、そして実際に処理された要素の数を表示してください。ヒント:onEachでカウンターを使用してください。