一文吃透 Kotlin 集合操作符

上一篇聊了 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
}

这个机制分为两个关键步骤。

  1. 创建经过优化的目标集合:在真正开始转换前,map 会先创建用于保存结果的目标列表,而且这个过程带有优化。

    它会调用 collectionSizeOrDefault(10)。这个辅助函数会检查当前 Iterable 是否为 Collection。如果是 Collection,例如 ListSet,它的大小就是已知的,于是 ArrayList 会用这个大小作为初始容量。这是一个关键的性能优化,因为它避免了元素不断加入时 ArrayList 多次扩容内部数组。如果它不是 Collection,例如 Sequence,大小无法提前得知,于是就默认创建容量为 10ArrayList

  2. 委托给 mapTo:随后,map 会立即调用 mapTo,并把刚创建的目标 ArrayListtransform lambda 传进去。真正的转换逻辑就在这个函数里:它负责遍历、转换,并把结果追加到给定的 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
}

这个机制其实比较简单:

  1. 创建目标集合:flatMap 会先创建一个标准的空 ArrayList<R>(),作为最终展平结果的目标集合。不同于 map,它很难提前计算最终大小,因为每个元素经过 transform 后都可能返回任意长度的 Iterable。因此,它只能从默认大小的 ArrayList 开始。

  2. 委托给 flatMapTo:随后它立即调用 flatMapTo,把新建的 ArrayListtransform lambda 传进去。实际的转换和展平逻辑都在这里:遍历源集合,应用转换,再把所有生成的元素追加到给定的 MutableCollection 中。

所以,flatMap 的内部机制就是委托加嵌套迭代。

flatMap 函数准备一个空 ArrayList,再把核心逻辑交给通用的 flatMapToflatMapTo 遍历源集合中的每个元素,转换得到一个中间集合,然后用 addAll 立即把这个中间集合里的所有元素追加到同一个最终目标列表中。

groupBy 和 associateBy

当你需要把列表重塑为 map 时,可以使用这两个函数。

groupBy 会保留每个 key 下的所有元素,也就是一个 value 列表;associateBy 对每个 key 只保留一个元素,key 冲突时后出现的元素会覆盖前面的元素。做聚合时优先用 groupBy;按唯一 key,或者"实际上唯一"的 key 快速查找时,优先用 associateBy

groupBy 会按照指定的 key 函数对元素分组,并返回一个包含分组列表的 mapassociateBy 则创建一个 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
}
  1. 创建目标 MapgroupBy 首先实例化保存最终结果的 Map。它特意选择了 LinkedHashMap,这是一个有意且重要的设计选择。LinkedHashMap 会保留 key 的插入顺序,也就是说,最终分组 map 中的 key 会按照每个 key 第一次在原始 Iterable 中出现的顺序排列。这能提供可预测、稳定的顺序,而这种特性通常很有价值。

  2. 委托给 groupByTo:随后它立即调用 groupByTo,并传入新创建的 LinkedHashMapkeySelector lambda。元素分类的核心逻辑在这个函数里:遍历源集合,为每个元素确定 key,并把元素放入目标 map 中对应的列表。

groupBy 的内部机制建立在委托和实用的 getOrPut 之上。主 groupBy 准备 LinkedHashMap 以保证 key 顺序稳定,然后把核心工作交给 groupByTogroupByTo 遍历源集合,用 keySelector 确定每个元素的 key,再通过 getOrPut 高效地查找或创建目标 map 中对应的 value 列表。

zip 和 unzip

如果你想按位置配对元素,可以使用 zip。它很适合合并 labels + values 这类并行列表,并且会在较短集合的长度处停止。

unzip 是它的逆操作:把 Pair 拆回两个列表。它在你已经把数据映射成 Pair,或者读取类似 CSV 的行之后很有用。

zip 会把两个集合组合成 Pairunzip 会把 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

  1. 简单的 zip(other) 中缀函数:这是最常见的版本,用于创建 List<Pair<T, R>>。它的实现只是一次委托:

    kotlin 复制代码
    public 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 的一个便捷特例。

  2. zip(other, transform) 主力实现:真正的合并逻辑位于这个核心实现中。它设计得更灵活,允许调用者精确定义每个索引位置上的两个元素应该如何组合。

    kotlin 复制代码
    public 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,例如 ListSetSequence

    • 优化的预分配:这是一个关键性能优化。函数会创建带初始容量的目标 ArrayList,容量设置为两个集合大小中的较小值。

      它会对两个 Iterable 都调用 collectionSizeOrDefault(10):如果 IterableCollection,就返回实际大小;否则返回默认值 10。通过 minOf(...),它能正确推导出 zipped 列表的最终大小,因为 zip 过程会在任意一个迭代器耗尽元素时停止。这种预分配避免了 ArrayList 调整内部数组大小,从而减少内存和性能开销。

    • 同步遍历循环:函数的核心是 while 循环。

      条件是 while (first.hasNext() && second.hasNext())。这正是 zip 操作的本质:只有两个迭代器都还有元素时,循环才会继续。一旦其中任意一个,即 first.hasNext()second.hasNext() 返回 false,循环就会终止。这自然保证了结果列表长度等于较短源集合的长度。循环体中,first.next()second.next() 会分别取出两个迭代器的下一个元素,然后把这两个元素传给 transform lambda,并将转换结果加入目标列表。

      这个单一循环在一次高效遍历中完成了同步遍历、转换和结果列表填充。

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
}
  1. 创建目标集合:filter 首先创建一个空的 ArrayList<T>(),作为保存通过过滤的元素的目标集合。和 map 这类函数不同,filter 无法提前知道结果列表最终会有多大,因此只能从默认大小的 ArrayList 开始。

  2. 委托给 filterTo:随后它立即调用 filterTo,把新建的 ArrayListpredicate lambda 传进去。循环处理完全部元素后,目标集合会被返回,其中只包含满足 predicate 的元素。

inline 的作用

大多数转换操作符,例如 mapflatMapgroupByfilter,都会被标记为 inline 函数。

这是一项重要的性能优化。通过内联,Kotlin 编译器可以避免在每个调用点为 predicate lambda 创建 Function 对象的运行时开销。

filter 为例,编译器会把 filter 循环的字节码和 predicate lambda 的函数体直接复制到调用 filter 的位置。因此,someList.filter { ... } 的性能几乎等同于手写一个带 if 条件的 for 循环,开发者可以使用清晰的声明式 API,而无需承担额外性能成本。

这种复制机制也带来了更高级的能力,因为 lambda 会继承调用函数的完整上下文。

例如,如果在 suspend 函数或 @Composable 函数内部调用 inline 函数,那么它的 lambda 参数也可以合法调用 suspend@Composable 函数;在普通的非内联 lambda 中,这种能力是不允许的。

这也解释了为什么你可以在这些转换操作符内部不受限制地调用 suspend 函数------inlinelambda 拿到了调用方的上下文。

一点想法

Kotlin 的集合转换操作符,例如 mapflatMapgroupByzip,为以函数式风格操作集合提供了实用工具。它们提升可读性,减少样板代码,并简化复杂数据结构的处理,使 Kotlin 标准库成为开发者的重要资源。

在实际项目中,这些操作符往往不是单独出现使用,而是会串联成一条数据处理管道。

比如先用 filter 剔除无效项,再用 map 把原始数据从一种形态转换成另一种,最后用 groupBy 做分组聚合,整个流程读起来就是一条清晰的声明式管道。配合 inline 带来的零开销特性,这种写法的运行时表现几乎和手写 for 循环没有差别,开发者可以把更多精力放在业务逻辑本身,而不是循环计数器和临时变量上。

掌握这些操作符,不只是写更短的代码,更是用更接近问题本身的方式来表达数据流转。

相关推荐
三少爷的鞋2 小时前
Main-safe:现代Android 架构真正的分水岭
android
沐怡旸11 小时前
深入解析 Android Performance Analyzer (APA) 底层架构与技术原理
android
李斯维18 小时前
从历史的角度看 Android 软件架构
android·架构·android jetpack
plainGeekDev21 小时前
Activity 间传值 → Navigation 参数
android·java·kotlin
用户416596736935521 小时前
Android WebView 加载 file:// 离线页面调试教程
android·前端
plainGeekDev21 小时前
onActivityResult → ActivityResult API
android·java·kotlin
随遇丿而安1 天前
第10周:Activity 基础功能与生命周期优化
android
alexhilton2 天前
Android车载OS中的Remote Compose
android·kotlin·android jetpack
落魄Android在线炒饭2 天前
Android 自定义HAL开发篇之 HIDL篇——从入门到实战(上)
android