集合操作符: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:一对多映射再展平

flatMapmap 多了一步「展平」。每个元素可以返回一个集合,最后所有小集合会被拼成一个大集合。

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 的合体。

注意:我曾经在解析嵌套 JSON 时滥用 flatMap,结果把层级关系搞丢了。flatMap 适合「一对多且不需要保留分组」的场景。如果你需要保留分组结构,考虑用 groupBy

3. filter:按条件筛选

filter 保留满足条件的元素,剔除不满足的。它不会改变元素本身,只做「留」或「扔」的决定。

val scores = listOf(88, 45, 92, 67, 73, 59)
val passed = scores.filter { it >= 60 }
// 结果: [88, 92, 67, 73]

我个人习惯把 filter 放在链式调用的最前面。先过滤掉不需要的数据,后面的操作符处理的数据量就小了,性能自然更好。

核心原则: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:带初始值的累积

foldreduce 很像,区别在于你可以指定一个初始值。这个初始值会成为累积的起点。

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,那会遍历两次集合。

知识体系总览

下面这张图帮你理清七个操作符的关系和适用场景:

集合操作符知识体系 输入集合 操作类型 转换类 筛选类 归约类 map(一对一) flatMap(一对多) filter(条件筛选) partition(二分筛选) groupBy(分组筛选) reduce(无初始值) fold(有初始值)

实战组合:链式调用

这些操作符真正的威力在于组合使用。我给你看一个真实项目中的例子:

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