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:最常用的两兄弟

mapfilter 是入门必会。我习惯把它们当作「流水线」来理解: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]

嗯,这里要注意:filtermap 的顺序会影响性能。先 filtermap,能减少 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 的每个操作符都会创建一个新集合。三步操作,三个中间集合。数据量小无所谓,但上百万条数据时,内存直接爆炸。

核心原则:数据量大(超过 10 万条)或链式操作超过 3 步,优先考虑 Sequence。

4.7 Sequence 惰性求值

Sequence 说白了就是「懒加载」版的集合。它不会立即执行操作,而是等到调用 toList()forEach 等终端操作时,才真正计算。

我画个图帮你理解:

List 处理流程(立即执行) 原始集合 filter 新集合 map 新集合 最终结果 中间集合 1 中间集合 2 Sequence 处理流程(惰性求值) 原始集合 filter 描述 map 描述 最终结果 只记录操作,不执行 只记录操作,不执行 toList() 触发执行

看到区别了吗?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个
个人经验:我习惯在 RecyclerView 的数据处理中用 Sequence。列表滑动时频繁触发过滤和排序,用 Sequence 能减少中间对象的创建,GC 压力小很多。

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