主题
第 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 Map7.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()) |
想跟集合方法(size、indexOf)混用 | ✅ | ❌ |
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 │ │惰性 │
└─────┘ └─────┘ └─────┘记忆要点:
- 默认全部用不可变集合(
listOf、setOf、mapOf) - 大数据 / 多步骤 / 短路场景 →
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.immutable 的 persistentListOf(...)。
Q3. map 和 flatMap 区别?
答:
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. reduce 跟 fold 区别?
答:
reduce:用第一个元素作为初值。空列表会抛UnsupportedOperationExceptionfold:你自己提供初值。空列表返回初值,更安全
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 } // 0Q5. groupBy 和 associateBy 啥区别?
答:
groupBy:value 是列表,多个元素能映射到同一个 keyassociateBy: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. Sequence 跟 List 的链式调用差别?
答:
| 维度 | 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. partition 跟 groupBy 啥区别?
答:
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. Map 的 getOrElse 和 getOrDefault 区别?
答:都是"取不到给默认值",但:
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}")
}