1. 概述
本文将深入解析 Kotlin 中 map()
、flatMap()
和 flatten()
三个集合操作函数的差异。这三个函数在日常开发中高频使用,尤其在处理嵌套结构数据时极易混淆。掌握它们的核心逻辑,能避免不少“踩坑”。
我们将通过实际代码示例说明各自的适用场景,并揭示 flatMap()
本质上是 map()
+ flatten()
的组合这一关键点。
2. map()
map()
是 Kotlin 集合库中的扩展函数,其定义如下:
fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R>
它的作用是对集合中的每个元素应用一个转换函数,将类型 T
转为 R
,最终返回一个 List<R>
。✅
典型用于一对一映射场景。
举个例子,假设我们有订单和订单明细的结构:
data class Order(val lines: List<OrderLine>)
data class OrderLine(val name: String, val price: Int)
现在有一个订单,想提取所有商品名称:
val order = Order(
listOf(OrderLine("Tomato", 2), OrderLine("Garlic", 3), OrderLine("Chives", 2))
)
val names = order.lines.map { it.name }
assertThat(names).containsExactly("Tomato", "Garlic", "Chives")
这里我们将 List<OrderLine>
映射为 List<String>
,每个元素一对一转换。
再比如计算订单总价:
val totalPrice = order.lines.map { it.price }.sum()
assertEquals(7, totalPrice)
map()
的底层逻辑等价于以下命令式写法:
val result = mutableListOf<R>()
for (each in this) {
result += transform(each)
}
但使用 map()
时你只需关注 转换逻辑,集合创建、遍历、添加等模板代码已被封装。
3. flatMap()
与 map()
不同,flatMap()
主要用于处理 一对多关系的扁平化。其函数签名如下:
fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R>
关键点在于:
- 输入:每个
T
元素被转换成一个Iterable<R>
- 输出:不是
List<Iterable<R>>
,而是将所有子集合“拍平”后得到List<R>
来看一个常见业务场景:多个订单中提取所有商品名。
val orders = listOf(
Order(listOf(OrderLine("Garlic", 1), OrderLine("Chives", 2))),
Order(listOf(OrderLine("Tomato", 3), OrderLine("Garlic", 4))),
Order(listOf(OrderLine("Potato", 5), OrderLine("Chives", 6))),
)
如果用 map()
:
orders.map { it.lines } // 结果是 List<List<OrderLine>> ❌
得到的是嵌套列表,无法直接操作。此时应使用 flatMap()
:
val lines: List<OrderLine> = orders.flatMap { it.lines }
val names = lines.map { it.name }.distinct()
assertThat(names).containsExactlyInAnyOrder("Garlic", "Chives", "Tomato", "Potato")
✅ flatMap { it.lines }
直接将每个订单的明细列表展开,合并为单一列表。
其命令式等价实现:
val result = mutableListOf<OrderLine>()
for (order in orders) {
val transformedList = order.lines
for (individual in transformedList) {
result += individual
}
}
显然,flatMap()
帮你省去了外层+内层双重循环的样板代码。
4. flatten()
flatten()
函数专用于 将嵌套的 Iterable
展开一层,不涉及任何元素转换。其定义如下:
public fun <T> Iterable<Iterable<T>>.flatten(): List<T>
示例:
val orderLines = listOf(
listOf(OrderLine("Garlic", 1), OrderLine("Chives", 2)),
listOf(OrderLine("Tomato", 3), OrderLine("Garlic", 4)),
listOf(OrderLine("Potato", 5), OrderLine("Chives", 6)),
)
val lines: List<OrderLine> = orderLines.flatten()
val expected = listOf(
OrderLine("Garlic", 1),
OrderLine("Chives", 2),
OrderLine("Tomato", 3),
OrderLine("Garlic", 4),
OrderLine("Potato", 5),
OrderLine("Chives", 6),
)
assertThat(lines).hasSize(6).isEqualTo(expected)
⚠️ 注意:
flatten()
不接受任何 lambda 参数- 它只做扁平化,不做映射
- 只展开一层嵌套(如
List<List<T>>
→List<T>
)
源码实现也很直观:
public fun <T> Iterable<Iterable<T>>.flatten(): List<T> {
val result = ArrayList<T>()
for (element in this) {
result.addAll(element)
}
return result
}
5. map()、flatMap() 与 flatten() 的关系
总结三者职责:
函数 | 作用 |
---|---|
map() |
转换,不扁平化 |
flatten() |
扁平化,不转换 |
flatMap() |
先转换,再扁平化 ✅ |
关键结论:
👉 flatMap()
等价于 map()
+ flatten()
验证一下:
val orders = listOf(
Order(listOf(OrderLine("Garlic", 1), OrderLine("Chives", 2))),
Order(listOf(OrderLine("Tomato", 3), OrderLine("Garlic", 4))),
Order(listOf(OrderLine("Potato", 5), OrderLine("Chives", 6))),
)
val expected = listOf(
OrderLine("Garlic", 1),
OrderLine("Chives", 2),
OrderLine("Tomato", 3),
OrderLine("Garlic", 4),
OrderLine("Potato", 5),
OrderLine("Chives", 6),
)
val resultMapAndFlatten: List<OrderLine> = orders.map { it.lines }.flatten()
val resultFlatMap: List<OrderLine> = orders.flatMap { it.lines }
assertThat(resultFlatMap).isEqualTo(resultMapAndFlatten).hasSize(6).isEqualTo(expected)
测试通过,两者结果完全一致。
因此你可以这样理解:
- 当你需要“先映射出子集合,再拍平”时,直接上
flatMap()
- 手动拆成
map().flatten()
虽然语义清晰,但多一次中间集合创建,略低效
6. 总结
- ✅
map()
:适合一对一转换,如字段提取、数值计算 - ✅
flatMap()
:解决一对多扁平化,典型用于嵌套集合展开 - ✅
flatten()
:纯扁平化工具,适用于已存在嵌套结构的场景 - 🔑
flatMap()
=map()
+flatten()
,优先使用前者更简洁高效
熟练掌握这三者的差异,能让你的 Kotlin 集合操作更加函数式、简洁且不易出错。
所有示例代码均可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-collections-2