4、集合操作符:map、filter、flatMap、reduce、fold、groupBy、sortedBy等常用操作符、链式调用与性能优化、Sequence惰性求值
集合操作符,说白了就是 Kotlin 给咱们准备的一把瑞士军刀。我刚开始从 Java 转 Kotlin 时,最爽的就是不用再写一长串 for 循环加临时变量了。一行链式调用,搞定。
但别高兴太早。用不好,性能会崩。咱们今天就把这些操作符掰开揉碎,讲清楚。
4.1 常用操作符速览
先列个表,心里有个底。这些操作符我几乎每天都在用。
| 操作符 | 作用 | 返回值 |
|---|---|---|
| map | 将每个元素转换成另一种形式 | 新 List |
| filter | 保留满足条件的元素 | 新 List |
| flatMap | 将每个元素展开成多个元素,再合并 | 新 List |
| reduce | 从左到右累积,无初始值 | 单个值 |
| fold | 从左到右累积,有初始值 | 单个值 |
| groupBy | 按条件分组 | Map |
| sortedBy | 按指定属性排序 | 新 List |
4.2 map 和 filter:最常用的两兄弟
map 和 filter 是入门必会。我习惯把它们当作「流水线」来理解:filter 是质检员,map 是加工员。
val numbers = listOf(1, 2, 3, 4, 5, 6)
// 先过滤出偶数,再平方
val result = numbers
.filter { it % 2 == 0 }
.map { it * it }
println(result) // [4, 16, 36]
嗯,这里要注意:filter 和 map 的顺序会影响性能。先 filter 再 map,能减少 map 处理的元素数量。我在项目中遇到过有人反过来写,数据量一大,卡得不行。
4.3 flatMap:把嵌套打平
flatMap 这个名字起得好——先 flat(打平)再 map(映射)。说白了,就是把一个集合里的每个元素展开成多个元素,然后合并成一个新集合。
data class Order(val items: List<String>)
val orders = listOf(
Order(listOf("手机", "充电器")),
Order(listOf("电脑", "鼠标", "键盘")),
Order(listOf("耳机"))
)
// 获取所有商品名称
val allItems = orders.flatMap { it.items }
println(allItems) // [手机, 充电器, 电脑, 鼠标, 键盘, 耳机]
我曾经在写购物车功能时,用 flatMap 把多个订单的商品合并展示,一行代码搞定。换成 Java 得写两层 for 循环。
4.4 reduce 和 fold:累积的艺术
这两个都是做累积操作的。区别在哪?reduce 没有初始值,fold 有。你想想看,如果集合是空的,reduce 会抛异常,而 fold 会返回初始值。
val numbers = listOf(1, 2, 3, 4, 5)
// reduce:从第一个元素开始累积
val sum1 = numbers.reduce { acc, i -> acc + i }
println(sum1) // 15
// fold:从初始值开始累积
val sum2 = numbers.fold(10) { acc, i -> acc + i }
println(sum2) // 25
// 空集合的情况
val empty = listOf<Int>()
// empty.reduce { a, b -> a + b } // 抛异常!
val safe = empty.fold(0) { acc, i -> acc + i }
println(safe) // 0
reduce 抛异常,就是因为某个接口返回了空列表。从那以后,我只要不确定集合是否为空,一律用 fold 加初始值。
4.5 groupBy 和 sortedBy:分组与排序
groupBy 按条件分组,返回一个 Map。我经常用它来做数据统计。
data class Student(val name: String, val score: Int)
val students = listOf(
Student("张三", 85),
Student("李四", 92),
Student("王五", 85),
Student("赵六", 78)
)
// 按分数分组
val grouped = students.groupBy { it.score }
println(grouped)
// {85=[张三, 王五], 92=[李四], 78=[赵六]}
sortedBy 按指定属性排序,默认升序。想降序就用 sortedByDescending。
val sorted = students.sortedByDescending { it.score }
println(sorted) // 按分数从高到低排序
4.6 链式调用与性能陷阱
链式调用很爽,但性能问题得注意。看这个例子:
val list = (1..1_000_000).toList()
// 问题写法:每一步都创建新集合
val result1 = list
.filter { it % 2 == 0 }
.map { it * 2 }
.filter { it > 1000 }
.map { it.toString() }
// 优化写法:用 Sequence
val result2 = list.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.filter { it > 1000 }
.map { it.toString() }
.toList()
为什么会这样?因为 List 的每个操作符都会创建一个新集合。三步操作,三个中间集合。数据量小无所谓,但上百万条数据时,内存直接爆炸。
4.7 Sequence 惰性求值
Sequence 说白了就是「懒加载」版的集合。它不会立即执行操作,而是等到调用 toList() 或 forEach 等终端操作时,才真正计算。
我画个图帮你理解:
看到区别了吗?List 是每一步都生成新集合,Sequence 是攒着一堆操作,最后一次性处理。我建议在以下场景用 Sequence:
- 数据量超过 10 万条
- 链式操作超过 3 步
- 只需要取前 N 条结果(
take配合 Sequence 能提前终止)
// Sequence 的提前终止优势
val result = (1..1_000_000).asSequence()
.filter { it % 2 == 0 }
.map { it * 3 }
.take(5) // 只取前5个,找到就停
.toList()
println(result) // [6, 12, 18, 24, 30]
// 如果是 List,会先处理完所有 100 万个元素,再取前5个
4.8 实战:综合运用
来个真实场景。假设我们要处理用户订单数据:
data class OrderItem(val productId: Int, val price: Double, val quantity: Int)
data class UserOrder(val userId: Int, val items: List<OrderItem>)
val orders = listOf(
UserOrder(1, listOf(
OrderItem(101, 29.9, 2),
OrderItem(102, 99.0, 1)
)),
UserOrder(2, listOf(
OrderItem(101, 29.9, 1),
OrderItem(103, 199.0, 1)
)),
UserOrder(3, listOf(
OrderItem(104, 15.0, 5)
))
)
// 需求:找出所有单价超过 50 元的商品,按价格降序排列,取前3个
val result = orders.asSequence()
.flatMap { it.items } // 展开所有订单项
.filter { it.price > 50 } // 过滤出高价商品
.sortedByDescending { it.price } // 按价格降序
.take(3) // 取前3个
.toList()
println(result)
// [OrderItem(103, 199.0, 1), OrderItem(102, 99.0, 1), OrderItem(104, 15.0, 5)]
你看,一行链式调用,逻辑清晰。换成 Java 的 for 循环,至少得写 20 行,还容易出错。
- 小数据量用 List 操作符,简单直观
- 大数据量或长链式操作用 Sequence,性能更好
- 先 filter 再 map,减少处理量
- 不确定集合是否为空时,用 fold 代替 reduce
- flatMap 是处理嵌套集合的利器
嗯,集合操作符就讲这么多。记住一点:代码是写给人看的,偶尔让机器跑得快一点。别为了炫技写出让人看不懂的链式调用,那就本末倒置了。
公众号:蓝海资料掘金营,微信deep3321