Kotlin: Kotlinのコレクション操作

最終更新:2026-08-26

Kotlinのコレクション演算を使えば、Charlieは命令型のループを宣言型のパイプラインに置き換えることができます。たった1行で15行分のforループを置き換えられ、意図もより明確になります。

1. 学習内容


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. 収集処理パイプライン

100%
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 flatMapmap + flatten には違いがありますか?
A 機能的には同等ですが、flatMap の方が効率的です(1ステップで完了します)。 これは、map + flatten を組み合わせた省略形です。

📖 まとめ


📝 練習問題

  1. 初心者 (⭐): filtermap を使用して、注文リストから金額が 1000 USD を超える注文の ID を抽出してください。ヒント: orders.filter { }.map { }
  2. 中級 (⭐⭐)groupBymapValues を使用して、各顧客の総支出額を計算し、金額の多い順に並べ替えてください。ヒント:groupBy { it.customer }.mapValues { }
  3. 上級 (⭐⭐⭐): Sequence を使用して、数百万件の注文に対する遅延処理パイプラインを実装してください:filter → map → take(100)、そして実際に処理された要素の数を表示してください。ヒント:onEach でカウンターを使用してください。

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%