Skip to content

第 7 章 集合 & 函数式操作

学习目标:能区分 List / Set / Map 及其可变版本;熟练用 map / filter / reduce / groupBy / partition / zip 等"集合瑞士军刀";理解 Sequence 惰性求值;写出"链式声明式"的现代 Kotlin 代码。


7.1 集合类型全景图

Kotlin 的集合分两大流派:可变 vs 不可变

                 ┌─────────────────────────────────────┐
                 │           Iterable<T>                │
                 └─────────┬───────────────────────────┘

            ┌──────────────┼──────────────┐
            │              │              │
       ┌────▼────┐    ┌────▼────┐   ┌────▼────┐
       │  List   │    │   Set   │   │   Map   │
       │ 有序可重 │    │ 无序唯一 │   │ 键值对    │
       └─┬───────┘    └──┬──────┘   └────┬────┘
         │               │                │
   ┌─────┼─────┐   ┌─────┼─────┐    ┌─────┼─────┐
   ▼     ▼     ▼   ▼     ▼     ▼    ▼     ▼     ▼
 List  Mutable Array Set Mutable    Map Mutable
       List              Set              Map

7.1.1 创建集合

kotlin
// 不可变(推荐默认用)
val list1 = listOf(1, 2, 3)
val set1 = setOf("a", "b", "c")
val map1 = mapOf("x" to 1, "y" to 2)

// 可变
val list2 = mutableListOf(1, 2, 3)
val set2 = mutableSetOf("a", "b")
val map2 = mutableMapOf("x" to 1)

// 空集合
val empty1 = emptyList<Int>()
val empty2 = emptyMap<String, Int>()

// 数组(基本类型有特殊版本,避免装箱)
val arr1 = arrayOf(1, 2, 3)
val arr2 = intArrayOf(1, 2, 3)
val arr3 = IntArray(5) { it * it }     // [0, 1, 4, 9, 16]

7.1.2 不可变集合的"陷阱"

kotlin
val list = listOf(1, 2, 3)
list.add(4)         // ❌ 编译报错:Unresolved reference: add
                    //   listOf 返回的 List 接口没有 add 方法

📌 重要:Kotlin 的不可变 List 只是只读视图,底层可能还是可变的(比如 ArrayList)。但你拿不到修改 API。

如果想要"真正的 immutable"(线程安全):用 kotlinx.collections.immutable 库的 persistentListOf(...)


7.2 集合的"瑞士军刀":常用操作

下面这些是每个 Kotlin 程序员每天都在用的操作。

7.2.1 转换:map / mapIndexed / flatMap

kotlin
val nums = listOf(1, 2, 3, 4, 5)

nums.map { it * 2 }                       // [2, 4, 6, 8, 10]
nums.mapIndexed { i, v -> "$i: $v" }      // ["0: 1", "1: 2", ...]
nums.mapNotNull { if (it > 2) it else null }  // [3, 4, 5]

// flatMap:每个元素映射成一个集合,最后摊平
listOf(1, 2, 3).flatMap { listOf(it, it * 10) }   // [1, 10, 2, 20, 3, 30]

val users = listOf(User("A", listOf("a@x.com")), User("B", listOf("b@x.com", "b2@x.com")))
users.flatMap { it.emails }                // ["a@x.com", "b@x.com", "b2@x.com"]

7.2.2 筛选:filter / take / drop / partition

kotlin
nums.filter { it % 2 == 0 }                // [2, 4]
nums.filterNot { it % 2 == 0 }             // [1, 3, 5]
nums.filterIndexed { i, _ -> i % 2 == 0 } // [1, 3, 5](按索引筛)

nums.take(3)                                // [1, 2, 3]   前 N 个
nums.drop(2)                                // [3, 4, 5]   去掉前 N 个
nums.takeWhile { it < 4 }                   // [1, 2, 3]   遇到不满足就停
nums.dropWhile { it < 3 }                   // [3, 4, 5]

// partition:一刀切两半
val (even, odd) = nums.partition { it % 2 == 0 }
// even=[2,4], odd=[1,3,5]

7.2.3 聚合:reduce / fold / sum / count

kotlin
nums.sum()                                  // 15
nums.average()                              // 3.0
nums.count()                                // 5
nums.count { it > 2 }                       // 3(带条件)
nums.min()                                  // 1
nums.max()                                  // 5

// reduce: 左折叠,第一个元素当初值
listOf(1, 2, 3, 4).reduce { acc, x -> acc + x }    // 1+2+3+4 = 10

// fold: 自己指定初值
listOf("a", "b", "c").fold("Start: ") { acc, x -> "$acc$x" }
// "Start: abc"

// sumOf: 比 sum 更通用
data class Item(val price: Double, val qty: Int)
items.sumOf { it.price * it.qty }

7.2.4 分组:groupBy / associateBy

kotlin
val words = listOf("apple", "banana", "apricot", "blueberry", "cherry")

// groupBy: 按 key 分组,value 是 List
words.groupBy { it.first() }
// {a=[apple, apricot], b=[banana, blueberry], c=[cherry]}

// associateBy: 按 key 索引,value 是单个(重复 key 后者覆盖前者)
words.associateBy { it.first() }
// {a=apricot, b=blueberry, c=cherry}

// associateWith: key 是元素,value 是计算出来的
words.associateWith { it.length }
// {apple=5, banana=6, ...}

7.2.5 排序:sorted / sortedBy / sortedWith

kotlin
nums.sorted()                               // [1, 2, 3, 4, 5]
nums.sortedDescending()                     // [5, 4, 3, 2, 1]

data class User(val name: String, val age: Int)
val users = listOf(User("C", 25), User("A", 30), User("B", 20))

users.sortedBy { it.age }                   // 按年龄
users.sortedByDescending { it.age }
users.sortedWith(compareBy({ it.age }, { it.name }))  // 多键排序

7.2.6 元素查找:find / first / any / all / none

kotlin
nums.find { it > 3 }                       // 4 (第一个匹配,找不到返回 null)
nums.first { it > 3 }                       // 4 (找不到抛 NoSuchElementException)
nums.firstOrNull { it > 100 }              // null

nums.any { it > 4 }                        // true
nums.all { it > 0 }                        // true
nums.none { it < 0 }                       // true

nums.contains(3)                            // true (等价于 3 in nums)

7.2.7 zip / unzip

kotlin
val names = listOf("Alice", "Bob", "Charlie")
val ages = listOf(25, 30, 35)

names.zip(ages)                             // [(Alice,25), (Bob,30), (Charlie,35)]
names.zip(ages) { n, a -> "$n is $a" }     // ["Alice is 25", ...]

// unzip
val pairs = listOf("a" to 1, "b" to 2)
val (keys, values) = pairs.unzip()         // keys=[a,b], values=[1,2]

7.2.8 chunked / windowed

kotlin
val list = (1..10).toList()

list.chunked(3)                            // [[1,2,3], [4,5,6], [7,8,9], [10]]

// 滑动窗口
list.windowed(3, step = 1)
// [[1,2,3], [2,3,4], [3,4,5], ...]

list.windowed(3, step = 2, partialWindows = true)
// [[1,2,3], [3,4,5], [5,6,7], [7,8,9], [9,10]]

7.3 链式调用:现代 Kotlin 风格

把多个操作链起来,每一步都是新集合

kotlin
data class Order(val userId: String, val amount: Double, val status: String)

val orders = listOf(
    Order("u1", 100.0, "PAID"),
    Order("u1", 50.0, "PAID"),
    Order("u2", 200.0, "REFUND"),
    Order("u2", 80.0, "PAID"),
    Order("u3", 30.0, "CANCEL"),
)

// 求每个用户的"已付款总金额",按金额倒序
val report = orders
    .filter { it.status == "PAID" }
    .groupBy { it.userId }
    .mapValues { (_, list) -> list.sumOf { it.amount } }
    .toList()
    .sortedByDescending { (_, total) -> total }

println(report)
// [(u1, 150.0), (u2, 80.0)]

📌 跟 Java 8 Stream 对比

  • Kotlin 集合操作直接在 List 上调用,不需要 .stream() / .collect()
  • Java Stream 是惰性的,Kotlin List 操作是急性的(每步都生成新 List)
  • 想要惰性 → 用 Sequence(下一节)

7.4 Sequence:惰性求值

7.4.1 痛点:链式调用的中间 List

kotlin
listOf(1, 2, 3, 4, 5)
    .filter { println("filter $it"); it % 2 == 0 }
    .map { println("map $it"); it * 2 }
    .first()
// 输出(注意 filter 把 5 个全过了一遍才开始 map):
// filter 1, filter 2, filter 3, filter 4, filter 5
// map 2, map 4
// → 4

每步都生成中间 List,处理大数据时浪费严重。

7.4.2 改用 Sequence

kotlin
listOf(1, 2, 3, 4, 5).asSequence()
    .filter { println("filter $it"); it % 2 == 0 }
    .map { println("map $it"); it * 2 }
    .first()
// 输出(filter 一个,map 一个,找到 first 立即停):
// filter 1, filter 2, map 2
// → 4

📌 Sequence 等价于 Java Stream:惰性 + 单遍历 + 不存中间结果。

7.4.3 何时用 Sequence?

场景用 List用 Sequence
几十个元素的小集合❌(启动开销划不来)
几千个以上元素
流水线步骤多(5+)
中途有 first / find 短路
需要返回 List 给别人用❌(最后还得 .toList()
想跟集合方法(sizeindexOf)混用

7.5 实战:电商订单分析

kotlin
data class Order(
    val id: String,
    val userId: String,
    val amount: Double,
    val items: List<String>,
    val status: String,
    val timestamp: Long,
)

fun report(orders: List<Order>) {
    // 1. 总收入
    val revenue = orders.filter { it.status == "PAID" }.sumOf { it.amount }
    println("总收入: ¥${"%.2f".format(revenue)}")
    
    // 2. Top 5 用户
    val top5 = orders
        .filter { it.status == "PAID" }
        .groupBy { it.userId }
        .mapValues { (_, list) -> list.sumOf { it.amount } }
        .toList()
        .sortedByDescending { (_, total) -> total }
        .take(5)
    
    println("\nTop 5 用户:")
    top5.forEach { (uid, total) -> println("  $uid: ¥$total") }
    
    // 3. 退款率
    val total = orders.size
    val refundCount = orders.count { it.status == "REFUND" }
    println("\n退款率: ${"%.1f%%".format(refundCount * 100.0 / total)}")
    
    // 4. 最热门商品(top 10)
    val topItems = orders
        .flatMap { it.items }
        .groupingBy { it }
        .eachCount()
        .toList()
        .sortedByDescending { it.second }
        .take(10)
    
    println("\nTop 10 商品:")
    topItems.forEach { (item, count) -> println("  $item: $count 单") }
}

7.6 章末小结

              ★ 第 7 章核心知识图谱 ★

        ┌──────────────┼──────────────┐
        │              │              │
     ┌──▼──┐       ┌──▼──┐        ┌──▼──┐
     │集合 │       │操作  │        │惰性 │
     ├─────┤       ├─────┤        ├─────┤
     │List │       │map  │        │Seque│
     │Set  │       │filter│       │nce  │
     │Map  │       │reduce│       │     │
     │Mutable│     │group │        │短路 │
     │     │       │sort  │        │惰性 │
     └─────┘       └─────┘        └─────┘

记忆要点

  • 默认全部用不可变集合(listOfsetOfmapOf
  • 大数据 / 多步骤 / 短路场景 → asSequence()
  • 链式调用比 for 循环更易读、更不易出错

🎤 7.7 章末面试题(10 道)

Q1. Kotlin 的 List 跟 Java 的 List 一样吗?

底层是同一个 java.util.List,但 Kotlin 在类型层面分了两个接口:

  • kotlin.collections.List(只读视图,没有 add/remove)
  • kotlin.collections.MutableList(可变,extends 上面)

编译后两个都是 Java 的 List。所以 Kotlin 的"不可变 List"只是没有暴露修改 API,不是真的不可变。


Q2. listOf(...) 返回的 List 真的不能修改吗?

不能通过它的 API 修改,但底层对象可能是可变的。例如:

kotlin
val list: List<Int> = mutableListOf(1, 2, 3)   // 创建可变,转成只读视图
list.add(4)                                      // ❌ 编译报错
(list as MutableList<Int>).add(4)                // ✅ 强转后可以改(脏招)

如果要真正的不可变(线程安全),用 kotlinx.collections.immutablepersistentListOf(...)


Q3. mapflatMap 区别?

  • map:1 个元素 → 1 个元素,结果还是同样长度的列表
  • flatMap:1 个元素 → N 个元素的列表,最后摊平成一个大列表
kotlin
listOf(1, 2, 3).map { listOf(it, it * 10) }
// [[1, 10], [2, 20], [3, 30]]    ← List<List<Int>>

listOf(1, 2, 3).flatMap { listOf(it, it * 10) }
// [1, 10, 2, 20, 3, 30]           ← List<Int>

Q4. reducefold 区别?

  • reduce:用第一个元素作为初值。空列表会抛 UnsupportedOperationException
  • fold:你自己提供初值。空列表返回初值,更安全
kotlin
listOf(1, 2, 3).reduce { acc, x -> acc + x }       // 6
listOf(1, 2, 3).fold(100) { acc, x -> acc + x }    // 106

emptyList<Int>().reduce { a, b -> a + b }          // 💥 抛异常
emptyList<Int>().fold(0) { a, b -> a + b }         // 0

Q5. groupByassociateBy 啥区别?

  • groupBy:value 是列表多个元素能映射到同一个 key
  • associateBy:value 是单个元素,重复 key 后者覆盖前者
kotlin
val list = listOf("apple", "ape", "banana")

list.groupBy { it.first() }
// {a=[apple, ape], b=[banana]}

list.associateBy { it.first() }
// {a=ape, b=banana}      ← apple 被 ape 覆盖了

Q6. SequenceList 的链式调用差别?

维度List 链式Sequence 链式
求值时机急性,每步生成中间 List惰性,到 terminal operation 才执行
中间内存大(每步一份)小(无中间集合)
短路不支持(每步必须算完)支持(first/find/any 提前停)
单元素开销略高(封装迭代器)
适用场景小集合、需要 size 等大集合、流水线长、有短路

💡 Java Stream ≈ Kotlin Sequence。


Q7. 什么场景该用 List?什么场景该用 Sequence?

  • 小数据(< 1000 元素)+ 流水线短(< 3 步) → 用 List
  • 大数据 / 流水线长 / 有 first/find 短路 → 用 Sequence
  • 需要返回结果给别人用 → 转 List:.toList()

实测:100 万元素 + filter + map + first → Sequence 比 List 快 ~10x。


Q8. partitiongroupBy 啥区别?

  • partition严格二分,返回 Pair<满足条件的, 不满足的>
  • groupBy:可以分任意多组,返回 Map<key, list>
kotlin
val (even, odd) = listOf(1,2,3,4,5).partition { it % 2 == 0 }
// even=[2,4], odd=[1,3,5]

listOf(1,2,3,4,5).groupBy { 
    when { it < 2 -> "small"; it < 5 -> "mid"; else -> "big" }
}
// {small=[1], mid=[2,3,4], big=[5]}

Q9. 怎么去重?怎么按某个属性去重?

kotlin
listOf(1, 2, 2, 3, 3, 3).distinct()     // [1, 2, 3]
listOf(1, 2, 2, 3).toSet()              // {1, 2, 3}(也去重,但变 Set)

data class User(val id: Int, val name: String)
val users = listOf(User(1, "A"), User(1, "B"), User(2, "C"))
users.distinctBy { it.id }              // [User(1,A), User(2,C)]  ← 按 id 去重

Q10. MapgetOrElsegetOrDefault 区别?

:都是"取不到给默认值",但:

  • getOrDefault(key, default):默认值是直接传值
  • getOrElse(key) { ... }:默认值是 lambda只在取不到时才计算
kotlin
val map = mapOf("a" to 1)

map.getOrDefault("b", computeExpensive())   // computeExpensive 总是会算
map.getOrElse("b") { computeExpensive() }   // 取得到就不算

map.getOrPut("b") { 42 }                     // 取不到塞进去(仅 MutableMap)

下一章 → 第 8 章 · 作用域函数 →

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

kotlin
/**
 * 第 7 章 · 集合 & 函数式操作 — 综合示例
 *
 * 涵盖:map/filter/reduce/groupBy/partition/zip/chunked/windowed/Sequence
 */

data class Order(
    val id: String,
    val userId: String,
    val amount: Double,
    val items: List<String>,
    val status: String,
)

fun main() {
    println("=== 1. 创建集合 ===")
    val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    val mset = mutableSetOf("a", "b", "c")
    val map = mapOf("x" to 1, "y" to 2)
    println("list = $list")
    println("set = $mset")
    println("map = $map")

    println("\n=== 2. 转换 ===")
    println("map { it * 2 } = ${list.map { it * 2 }}")
    println("mapIndexed = ${list.mapIndexed { i, v -> "[${i}]=${v}" }}")
    println("flatMap = ${list.take(3).flatMap { listOf(it, it * 10) }}")

    println("\n=== 3. 筛选 ===")
    println("偶数: ${list.filter { it % 2 == 0 }}")
    val (even, odd) = list.partition { it % 2 == 0 }
    println("partition: even=$even, odd=$odd")
    println("take(3) = ${list.take(3)}")
    println("drop(7) = ${list.drop(7)}")
    println("takeWhile{<5} = ${list.takeWhile { it < 5 }}")

    println("\n=== 4. 聚合 ===")
    println("sum = ${list.sum()}")
    println("average = ${list.average()}")
    println("count{>5} = ${list.count { it > 5 }}")
    println("reduce: 1+2+...+10 = ${list.reduce { acc, x -> acc + x }}")
    println("fold(100): 100 + 1+2+...+10 = ${list.fold(100) { acc, x -> acc + x }}")

    println("\n=== 5. 排序 ===")
    val users = listOf(
        "Charlie" to 25,
        "Alice" to 30,
        "Bob" to 20,
    )
    println("按年龄: ${users.sortedBy { it.second }}")
    println("按名字倒序: ${users.sortedByDescending { it.first }}")

    println("\n=== 6. 分组 ===")
    val words = listOf("apple", "banana", "apricot", "blueberry", "cherry")
    println("groupBy 首字母:")
    words.groupBy { it.first() }.forEach { (k, v) -> println("  $k -> $v") }

    println("associateBy 首字母(重复 key 后者覆盖):")
    println("  ${words.associateBy { it.first() }}")

    println("associateWith 长度:")
    println("  ${words.associateWith { it.length }}")

    println("\n=== 7. zip / unzip / chunked / windowed ===")
    val names = listOf("a", "b", "c")
    val ages = listOf(1, 2, 3)
    println("zip: ${names.zip(ages)}")
    println("zip + transform: ${names.zip(ages) { n, a -> "$n=$a" }}")
    println("chunked(3): ${list.chunked(3)}")
    println("windowed(3): ${list.windowed(3).take(3)}")

    println("\n=== 8. Sequence vs List ===")
    println("--- List 急性 ---")
    listOf(1, 2, 3, 4, 5)
        .filter { print("F$it "); it % 2 == 0 }
        .map { print("M$it "); it * 2 }
        .first()
    println()

    println("--- Sequence 惰性 ---")
    listOf(1, 2, 3, 4, 5).asSequence()
        .filter { print("F$it "); it % 2 == 0 }
        .map { print("M$it "); it * 2 }
        .first()
    println()
    println("👆 Sequence 在找到第一个就停了!")

    println("\n=== 9. 实战:电商订单分析 ===")
    val orders = listOf(
        Order("o001", "u1", 100.0, listOf("apple"), "PAID"),
        Order("o002", "u1", 50.0, listOf("banana", "cherry"), "PAID"),
        Order("o003", "u2", 200.0, listOf("apple", "banana"), "REFUND"),
        Order("o004", "u2", 80.0, listOf("apple"), "PAID"),
        Order("o005", "u3", 30.0, listOf("cherry"), "CANCEL"),
        Order("o006", "u3", 150.0, listOf("banana"), "PAID"),
    )

    val revenue = orders.filter { it.status == "PAID" }.sumOf { it.amount }
    println("总收入: ¥$revenue")

    println("\n按用户统计已付金额:")
    orders
        .filter { it.status == "PAID" }
        .groupBy { it.userId }
        .mapValues { (_, list) -> list.sumOf { it.amount } }
        .toList()
        .sortedByDescending { it.second }
        .forEach { (uid, total) -> println("  $uid: ¥$total") }

    println("\n商品销量 Top 3:")
    orders
        .flatMap { it.items }
        .groupingBy { it }
        .eachCount()
        .toList()
        .sortedByDescending { it.second }
        .take(3)
        .forEach { (item, cnt) -> println("  $item: $cnt 单") }

    val (paid, others) = orders.partition { it.status == "PAID" }
    println("\n已付订单数: ${paid.size} / 其他: ${others.size}")
}

CollectionDemo.kt ↗