集合操作符:map、flatMap、filter、reduce、fold、groupBy、partition
集合操作符,说白了就是 Kotlin 给咱们准备的一套「数据加工流水线」。我刚开始用 Java 写集合处理时,动不动就是 for 循环套 if 判断,代码又臭又长。后来切到 Kotlin,发现一行链式调用就能搞定,那种感觉——嗯,就像从手动挡换成了自动挡。
今天咱们就把最常用的七个操作符捋一遍。我会结合自己踩过的坑,帮你把每个操作符的脾气摸透。
1. map:一对一映射
map 是最基础的操作符。它把集合里的每个元素,按照你给的规则「变」成另一个东西。结果集合的元素个数不变。
val numbers = listOf(1, 2, 3, 4, 5)
val squared = numbers.map { it * it }
// 结果: [1, 4, 9, 16, 25]
我在项目中遇到过一个问题:用 map 处理大量数据时,每次都会生成一个新列表。如果你连续调五六个 map,中间会产生多个临时集合。性能敏感的场景下,可以考虑用 asSequence() 转成惰性序列。
mapNotNull,它会自动过滤掉 null 值。
2. flatMap:一对多映射再展平
flatMap 比 map 多了一步「展平」。每个元素可以返回一个集合,最后所有小集合会被拼成一个大集合。
val words = listOf("hello", "world")
val letters = words.flatMap { it.toList() }
// 结果: [h, e, l, l, o, w, o, r, l, d]
你想想看,如果这里用 map,得到的是 List<List<Char>>,还得再调一次 flatten()。flatMap 就是 map + flatten 的合体。
groupBy。
3. filter:按条件筛选
filter 保留满足条件的元素,剔除不满足的。它不会改变元素本身,只做「留」或「扔」的决定。
val scores = listOf(88, 45, 92, 67, 73, 59)
val passed = scores.filter { it >= 60 }
// 结果: [88, 92, 67, 73]
我个人习惯把 filter 放在链式调用的最前面。先过滤掉不需要的数据,后面的操作符处理的数据量就小了,性能自然更好。
4. reduce:累积归约
reduce 从第一个元素开始,依次和后面的元素做「累积操作」。最终得到一个单一值。
val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.reduce { acc, i -> acc + i }
// 过程: 1+2=3, 3+3=6, 6+4=10, 10+5=15
// 结果: 15
reduce 会抛出 UnsupportedOperationException。我曾经在线上环境踩过这个坑——一个本该有数据的列表,因为某个边界条件变成了空列表,结果直接崩了。安全起见,用 reduceOrNull 或者先判空。
5. fold:带初始值的累积
fold 和 reduce 很像,区别在于你可以指定一个初始值。这个初始值会成为累积的起点。
val numbers = listOf(1, 2, 3, 4, 5)
val sumFromTen = numbers.fold(10) { acc, i -> acc + i }
// 过程: 10+1=11, 11+2=13, 13+3=16, 16+4=20, 20+5=25
// 结果: 25
我建议在需要「默认值」的场景下优先用 fold。比如统计订单总金额,如果订单列表为空,fold(0.0) 返回 0.0,而 reduce 直接崩了。
| 特性 | reduce | fold |
|---|---|---|
| 初始值 | 无(用第一个元素) | 有(由你指定) |
| 空集合 | 抛异常 | 返回初始值 |
| 返回值类型 | 与元素类型相同 | 可与元素类型不同 |
6. groupBy:分组
groupBy 根据你指定的「分组键」把元素归类。结果是一个 Map,键是分组条件,值是对应元素的列表。
data class Person(val name: String, val age: Int)
val people = listOf(
Person("Alice", 25),
Person("Bob", 30),
Person("Charlie", 25),
Person("David", 30)
)
val grouped = people.groupBy { it.age }
// 结果: {25=[Alice, Charlie], 30=[Bob, David]}
我在项目中用 groupBy 做过一个功能:把用户按注册月份分组,然后统计每个月的注册量。一行代码搞定分组,比写 for 循环加 HashMap 清爽太多了。
7. partition:一分为二
partition 根据条件把集合分成两个列表。第一个列表放满足条件的元素,第二个放不满足的。它返回一个 Pair。
val numbers = listOf(1, 2, 3, 4, 5, 6)
val (evens, odds) = numbers.partition { it % 2 == 0 }
// evens: [2, 4, 6]
// odds: [1, 3, 5]
解构声明 val (a, b) = ... 是 Kotlin 的语法糖,用起来特别顺手。我经常用 partition 做「通过/未通过」「有效/无效」这样的二分处理。
filter。如果你同时需要「满足」和「不满足」的两部分,用 partition。别调两次 filter,那会遍历两次集合。
知识体系总览
下面这张图帮你理清七个操作符的关系和适用场景:
实战组合:链式调用
这些操作符真正的威力在于组合使用。我给你看一个真实项目中的例子:
data class Order(val id: Long, val amount: Double, val status: String)
val orders = listOf(
Order(1, 299.0, "paid"),
Order(2, 150.0, "pending"),
Order(3, 499.0, "paid"),
Order(4, 89.0, "cancelled"),
Order(5, 320.0, "paid")
)
// 需求:已支付订单的总金额,并分组统计
val result = orders
.filter { it.status == "paid" } // 先筛选
.map { it.amount } // 提取金额
.fold(0.0) { acc, amount -> acc + amount } // 累加
println("已支付订单总金额: $result") // 输出: 1118.0
你看,三行链式调用,清晰表达了「筛选 → 提取 → 累加」的完整流程。换成 Java 的 for 循环,至少得七八行,还得维护临时变量。
好了,这七个操作符就聊到这儿。说白了,它们就是 Kotlin 给咱们的「瑞士军刀」——单个用能干活,组合用能造火箭。你写代码时多想想「我要对数据做什么」,然后挑对应的操作符,代码自然就干净了。
公众号:蓝海资料掘金营,微信deep3321