上一篇聊了 Kotlin 的集合,这篇继续看它的转换操作符。可能很多人不清楚这些操作符都有哪些,下面就带大家一探究竟。
Kotlin 提供了一系列集合转换操作符,帮助你更高效地处理集合。这些操作符会基于原集合创建新集合,并应用不同的转换逻辑。

map 和 mapIndexed
map 会对集合中的每个元素应用一个函数,并返回包含转换结果的新集合。如果转换时同时需要元素和索引,可以使用 mapIndexed。
当你需要把一组原始数据经过若干操作转换成另一组数据时,它非常有用,也是 Kotlin 项目中最常见的函数之一。
kotlin
val names = listOf("skydoves", "kotlin", "developer")
val uppercased = names.map { it.uppercase() }
println(uppercased) // 输出:[SKYDOVES, KOTLIN, DEVELOPER]
map 函数本身并不承载核心转换逻辑。它更像是一个准备性的包装器,把主要工作交给更通用的辅助函数 mapTo。
来看它的源码:
kotlin
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
// 1. 创建一个具有优化初始容量的目标列表。
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
internal fun <T> Iterable<T>.collectionSizeOrDefault(default: Int): Int =
if (this is Collection<*>) this.size else default
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}
这个机制分为两个关键步骤。
-
创建经过优化的目标集合:在真正开始转换前,
map会先创建用于保存结果的目标列表,而且这个过程带有优化。它会调用
collectionSizeOrDefault(10)。这个辅助函数会检查当前Iterable是否为Collection。如果是Collection,例如List或Set,它的大小就是已知的,于是ArrayList会用这个大小作为初始容量。这是一个关键的性能优化,因为它避免了元素不断加入时ArrayList多次扩容内部数组。如果它不是Collection,例如Sequence,大小无法提前得知,于是就默认创建容量为10的ArrayList。 -
委托给
mapTo:随后,map会立即调用mapTo,并把刚创建的目标ArrayList和transformlambda传进去。真正的转换逻辑就在这个函数里:它负责遍历、转换,并把结果追加到给定的MutableCollection。
因此,map 的内部机制是一个清晰高效的两步流程。它先像一个工厂一样,根据源 Iterable 的大小是否已知来创建初始容量合适的 ArrayList;然后把核心工作委托给更通用的 mapTo,由后者通过一个简单循环完成转换并填充目标列表。
flatMap 和 flatten
flatMap 会把每个元素转换成另一个集合,然后把这些结果展平成一个列表。flatten 则直接作用于嵌套集合。
kotlin
val nestedLists = listOf(
listOf("kotlin", "android"),
listOf("developer", "tools")
)
val flattened = nestedLists.flatMap { it }
println(flattened) // 输出:[kotlin, android, developer, tools]
和 map 类似,顶层的 flatMap 函数本身也不包含核心转换逻辑。它同样是一个准备性的包装器,把主要工作委托给更通用的辅助函数。
kotlin
public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
// 1. 创建一个默认的目标列表。
return flatMapTo(ArrayList<R>(), transform)
}
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
// 1. 外层循环:遍历源 iterable 中的每个元素。
for (element in this) {
// 2. 应用 transform,得到一个内部 iterable。
val list = transform(element)
// 3. 内部"循环":把内部 iterable 中的所有元素添加到目标集合。
destination.addAll(list)
}
return destination
}
这个机制其实比较简单:
-
创建目标集合:
flatMap会先创建一个标准的空ArrayList<R>(),作为最终展平结果的目标集合。不同于map,它很难提前计算最终大小,因为每个元素经过transform后都可能返回任意长度的Iterable。因此,它只能从默认大小的ArrayList开始。 -
委托给
flatMapTo:随后它立即调用flatMapTo,把新建的ArrayList和transformlambda传进去。实际的转换和展平逻辑都在这里:遍历源集合,应用转换,再把所有生成的元素追加到给定的MutableCollection中。
所以,flatMap 的内部机制就是委托加嵌套迭代。
主 flatMap 函数准备一个空 ArrayList,再把核心逻辑交给通用的 flatMapTo。flatMapTo 遍历源集合中的每个元素,转换得到一个中间集合,然后用 addAll 立即把这个中间集合里的所有元素追加到同一个最终目标列表中。
groupBy 和 associateBy
当你需要把列表重塑为 map 时,可以使用这两个函数。
groupBy 会保留每个 key 下的所有元素,也就是一个 value 列表;associateBy 对每个 key 只保留一个元素,key 冲突时后出现的元素会覆盖前面的元素。做聚合时优先用 groupBy;按唯一 key,或者"实际上唯一"的 key 快速查找时,优先用 associateBy。
groupBy 会按照指定的 key 函数对元素分组,并返回一个包含分组列表的 map。associateBy 则创建一个 map,其中每个 key 来自元素的某个属性,value 是元素本身。
kotlin
val developers = listOf("Alice", "Bob", "Charlie")
val groupedByLength = developers.groupBy { it.length }
println(groupedByLength) // 输出:{5=[Alice, Bob], 7=[Charlie]}
公开的 groupBy 函数是一个简洁的包装器。它先准备目标 Map,再把主要工作委托给 groupByTo。
kotlin
public inline fun <T, K> Iterable<T>.groupBy(keySelector: (T) -> K): Map<K, List<T>> {
// 1. 创建目标 map。
return groupByTo(LinkedHashMap<K, MutableList<T>>(), keySelector)
}
public inline fun <T, K, M : MutableMap<in K, MutableList<T>>> Iterable<T>.groupByTo(destination: M, keySelector: (T) -> K): M {
// 1. 遍历每个元素。
for (element in this) {
// 2. 确定当前元素的 key。
val key = keySelector(element)
// 3. 获取或创建这个 key 对应的列表。
val list = destination.getOrPut(key) { ArrayList<T>() }
// 4. 把元素添加到对应的列表中。
list.add(element)
}
return destination
}
-
创建目标
Map:groupBy首先实例化保存最终结果的Map。它特意选择了LinkedHashMap,这是一个有意且重要的设计选择。LinkedHashMap会保留key的插入顺序,也就是说,最终分组map中的key会按照每个key第一次在原始Iterable中出现的顺序排列。这能提供可预测、稳定的顺序,而这种特性通常很有价值。 -
委托给
groupByTo:随后它立即调用groupByTo,并传入新创建的LinkedHashMap和keySelectorlambda。元素分类的核心逻辑在这个函数里:遍历源集合,为每个元素确定key,并把元素放入目标map中对应的列表。
groupBy 的内部机制建立在委托和实用的 getOrPut 之上。主 groupBy 准备 LinkedHashMap 以保证 key 顺序稳定,然后把核心工作交给 groupByTo。groupByTo 遍历源集合,用 keySelector 确定每个元素的 key,再通过 getOrPut 高效地查找或创建目标 map 中对应的 value 列表。
zip 和 unzip
如果你想按位置配对元素,可以使用 zip。它很适合合并 labels + values 这类并行列表,并且会在较短集合的长度处停止。
unzip 是它的逆操作:把 Pair 拆回两个列表。它在你已经把数据映射成 Pair,或者读取类似 CSV 的行之后很有用。
zip 会把两个集合组合成 Pair。unzip 会把 Pair 集合拆成两个独立列表。
kotlin
val languages = listOf("Kotlin", "Java")
val versions = listOf("1.8", "11")
val paired = languages.zip(versions)
println(paired) // 输出:[(Kotlin, 1.8), (Java, 11)]
Kotlin 为 zip 提供了两个主要重载,体现了一种常见设计:在更通用、更灵活的 API 之上,构建一个简单方便的 API。
-
简单的
zip(other)中缀函数:这是最常见的版本,用于创建List<Pair<T, R>>。它的实现只是一次委托:kotlinpublic infix fun <T, R> Iterable<T>.zip(other: Iterable<R>): List<Pair<T, R>> { return zip(other) { t1, t2 -> t1 to t2 } }委托:这个函数没有任何迭代逻辑,而是立即调用更强大的
zip(other, transform)重载。默认
transform:{ t1, t2 -> t1 to t2 }。这个lambda只负责接收两个元素,并把它们组成一个标准的Pair。因此,简单版zip就是带transform的通用zip的一个便捷特例。 -
zip(other, transform)主力实现:真正的合并逻辑位于这个核心实现中。它设计得更灵活,允许调用者精确定义每个索引位置上的两个元素应该如何组合。kotlinpublic inline fun <T, R, V> Iterable<T>.zip(other: Iterable<R>, transform: (a: T, b: R) -> V): List<V> { // 1. 获取两个集合的迭代器 val first = iterator() val second = other.iterator() // 2. 预先分配结果列表 val list = ArrayList<V>(minOf(collectionSizeOrDefault(10), other.collectionSizeOrDefault(10))) // 3. 合并循环 while (first.hasNext() && second.hasNext()) { list.add(transform(first.next(), second.next())) } return list }拆开来看,这个核心机制包含以下部分。
-
直接创建
Iterator:第一步是分别为接收者,也就是this,以及另一个Iterable获取Iterator。通过直接使用迭代器,这个函数能以统一方式处理任意Iterable,例如List、Set或Sequence。 -
优化的预分配:这是一个关键性能优化。函数会创建带初始容量的目标
ArrayList,容量设置为两个集合大小中的较小值。它会对两个
Iterable都调用collectionSizeOrDefault(10):如果Iterable是Collection,就返回实际大小;否则返回默认值10。通过minOf(...),它能正确推导出 zipped 列表的最终大小,因为 zip 过程会在任意一个迭代器耗尽元素时停止。这种预分配避免了ArrayList调整内部数组大小,从而减少内存和性能开销。 -
同步遍历循环:函数的核心是
while循环。条件是
while (first.hasNext() && second.hasNext())。这正是zip操作的本质:只有两个迭代器都还有元素时,循环才会继续。一旦其中任意一个,即first.hasNext()或second.hasNext()返回false,循环就会终止。这自然保证了结果列表长度等于较短源集合的长度。循环体中,first.next()和second.next()会分别取出两个迭代器的下一个元素,然后把这两个元素传给transformlambda,并将转换结果加入目标列表。这个单一循环在一次高效遍历中完成了同步遍历、转换和结果列表填充。
-
filter、filterNot 和 filterIndexed
这些函数用于通过清晰、有表达力的谓词来选择或排除元素。
filter 保留匹配项。
filterNot 移除匹配项,通常比写 !condition 更清晰。
当你还需要元素位置时,比如"保留每第 3 个元素"或"跳过第一个匹配项",可以使用 filterIndexed。在按指定条件过滤 Iterable 时,它们非常实用,也很常见。
filter 会保留满足指定条件的元素。filterNot 会排除满足条件的元素。filterIndexed 会同时考虑元素及其索引。
kotlin
val items = listOf("skydoves", "kotlin", "android")
val filtered = items.filter { it.startsWith("k") }
println(filtered) // 输出:[kotlin]
公开的 filter 函数是一个简洁的包装器。它负责设置操作,然后把控制权交出去。
kotlin
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
// 1. 创建一个默认的目标列表。
return filterTo(ArrayList<T>(), predicate)
}
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
// 1. 遍历源 iterable 中的每个元素。
for (element in this)
// 2. 应用 predicate。
if (predicate(element))
// 3. 如果 predicate 为 true,就把元素添加到目标集合。
destination.add(element)
return destination
}
-
创建目标集合:
filter首先创建一个空的ArrayList<T>(),作为保存通过过滤的元素的目标集合。和map这类函数不同,filter无法提前知道结果列表最终会有多大,因此只能从默认大小的ArrayList开始。 -
委托给
filterTo:随后它立即调用filterTo,把新建的ArrayList和predicatelambda传进去。循环处理完全部元素后,目标集合会被返回,其中只包含满足predicate的元素。
inline 的作用
大多数转换操作符,例如 map、flatMap、groupBy 和 filter,都会被标记为 inline 函数。
这是一项重要的性能优化。通过内联,Kotlin 编译器可以避免在每个调用点为 predicate lambda 创建 Function 对象的运行时开销。
以 filter 为例,编译器会把 filter 循环的字节码和 predicate lambda 的函数体直接复制到调用 filter 的位置。因此,someList.filter { ... } 的性能几乎等同于手写一个带 if 条件的 for 循环,开发者可以使用清晰的声明式 API,而无需承担额外性能成本。
这种复制机制也带来了更高级的能力,因为 lambda 会继承调用函数的完整上下文。
例如,如果在 suspend 函数或 @Composable 函数内部调用 inline 函数,那么它的 lambda 参数也可以合法调用 suspend 或 @Composable 函数;在普通的非内联 lambda 中,这种能力是不允许的。
这也解释了为什么你可以在这些转换操作符内部不受限制地调用 suspend 函数------inline 让 lambda 拿到了调用方的上下文。
一点想法
Kotlin 的集合转换操作符,例如 map、flatMap、groupBy 和 zip,为以函数式风格操作集合提供了实用工具。它们提升可读性,减少样板代码,并简化复杂数据结构的处理,使 Kotlin 标准库成为开发者的重要资源。
在实际项目中,这些操作符往往不是单独出现使用,而是会串联成一条数据处理管道。
比如先用 filter 剔除无效项,再用 map 把原始数据从一种形态转换成另一种,最后用 groupBy 做分组聚合,整个流程读起来就是一条清晰的声明式管道。配合 inline 带来的零开销特性,这种写法的运行时表现几乎和手写 for 循环没有差别,开发者可以把更多精力放在业务逻辑本身,而不是循环计数器和临时变量上。
掌握这些操作符,不只是写更短的代码,更是用更接近问题本身的方式来表达数据流转。