1. 简介

在日常 Kotlin 开发中,集合操作无处不在。我们经常需要对集合元素或整个集合进行转换处理。幸运的是,Kotlin 标准库提供了丰富的内置方法,让我们能更专注于业务逻辑本身,而不是重复造轮子

这些 API 设计优雅、链式调用自然,掌握它们不仅能提升编码效率,还能写出更具可读性的函数式风格代码。本文将系统梳理常用集合转换操作,帮助你在实际项目中游刃有余。


2. 过滤元素

过滤是最基础的集合变换之一,即保留满足条件的元素,剔除不符合要求的数据——可以理解为“过筛子”。

基础过滤:filter

使用 filter 方法传入一个 lambda 表达式作为判断条件(谓词),只有返回 true 的元素才会被保留:

val input = listOf(1, 2, 3, 4, 5)
val filtered = input.filter { it <= 3 }
assertEquals(listOf(1, 2, 3), filtered)

你也可以引用外部函数:

fun isSmall(num: Int) = num < 4

val filtered = input.filter(::isSmall)

反向过滤:filterNot

如果想取反条件,可以直接用 filterNot,比手动加 ! 更清晰:

val large = input.filter { !isSmall(it) }
val alsoLarge = input.filterNot(::isSmall)
assertEquals(listOf(4, 5), alsoLarge)

✅ 推荐使用 filterNot 而非 filter { !xxx },语义更明确。

按索引过滤:filterIndexed

某些场景下需要结合元素索引做判断,此时可用 filterIndexed

val input = listOf(5, 4, 3, 2, 1)
val filtered = input.filterIndexed { index, element -> index < 3 }
assertEquals(listOf(5, 4, 3), filtered)

特殊过滤方法

Kotlin 还提供了一些类型安全的专用过滤方法:

  • filterNotNull():从 List<T?> 中提取非空值,结果为 List<T>
  • filterIsInstance<T>():筛选出指定子类型的实例,常用于泛型擦除后的类型还原

示例:

val nullable: List<String?> = listOf("a", null, "b")
val nonnull: List<String> = nullable.filterNotNull() // ["a", "b"]

open class Animal
class Dog : Animal()
val animals: List<Animal> = listOf(Dog(), Animal(), Dog())
val dogs: List<Dog> = animals.filterIsInstance<Dog>()

⚠️ 注意这两个方法会改变集合的实际类型,属于“带转型的过滤”。


3. 映射元素(Map)

除了过滤,我们还经常需要把一种类型的数据映射成另一种形式,这就是 map 的核心用途。

基本映射:map

map 接收一个转换函数,将每个元素逐一转换后生成新集合:

val input = listOf("one", "two", "three")

val reversed = input.map { it.reversed() }
assertEquals(listOf("eno", "owt", "eerht"), reversed)

val lengths = input.map { it.length }
assertEquals(listOf(3, 3, 5), lengths)

无论是同类型变换还是转为完全不同类型(如 String → Int),API 使用方式完全一致。

带索引映射:mapIndexed

若需访问元素索引,使用 mapIndexed

val input = listOf(3, 2, 1)
val result = input.mapIndexed { index, value -> index * value }
assertEquals(listOf(0, 2, 2), result)

处理可能为空的结果:mapNotNull

当映射函数可能返回 null 时,可用 mapNotNull 自动过滤掉空值:

val input = listOf(1, 2, 3, 4, 5)
val smallSquares = input.mapNotNull { 
    if (it <= 3) it * it else null
}
assertEquals(listOf(1, 4, 9), smallSquares)

✅ 相比先 mapfilterNotNull()mapNotNull 性能更好且更简洁。

3.1 Map 类型的转换

对于 Map<K, V>,Kotlin 提供了专门的方法来转换键或值:

方法 作用
mapKeys { } 转换 map 的 key
mapValues { } 转换 map 的 value

示例:

val inputs = mapOf("one" to 1, "two" to 2, "three" to 3)

val squares = inputs.mapValues { it.value * it.value }
assertEquals(mapOf("one" to 1, "two" to 4, "three" to 9), squares)

val uppercases = inputs.mapKeys { it.key.toUpperCase() }
assertEquals(mapOf("ONE" to 1, "TWO" to 2, "THREE" to 3), uppercases)

⚠️ 注意:mapKeys 若产生重复 key,后面的会覆盖前面的,类似于普通 map put 行为。


4. 展平集合(Flattening)

有时 map 的结果是一个嵌套集合(List<List<T>>),我们需要将其展平为单层结构。

手动展平:flatten

val inputs = listOf("one", "two", "three")
val characters = inputs.map(String::toList)
// 结果:[[o,n,e], [t,w,o], [t,h,r,e,e]]

val flattened = characters.flatten()
assertEquals(listOf('o','n','e','t','w','o','t','h','r','e','e'), flattened)

合并操作:flatMap

flatMapmap + flatten 的组合拳,一步到位:

val inputs = listOf("one", "two", "three")
val characters = inputs.flatMap(String::toList)
assertEquals(listOf('o','n','e','t','w','o','t','h','r','e','e'), characters)

✅ 实际开发中 flatMap 更常用,避免中间集合开销。


5. 合并两个集合:Zipping

有时候需要将两个集合按位置配对,形成一一对应的关系,这就是 zip 操作。

基础 zip

val left = listOf("one", "two", "three")
val right = listOf(1, 2, 3)
val zipped = left.zip(right)
// 结果:[(one,1), (two,2), (three,3)]

⚠️ 如果两集合长度不同,结果以较短者为准:

val left = listOf("one", "two")
val right = listOf(1, 2, 3)
val zipped = left.zip(right)
assertEquals(listOf(Pair("one", 1), Pair("two", 2)), zipped)

解压:unzip

zip 相反,unzip 可将 List<Pair<A,B>> 拆分为两个列表:

val zipped = listOf(Pair("Alice", 25), Pair("Bob", 30))
val (names, ages) = zipped.unzip()

assertEquals(listOf("Alice", "Bob"), names)
assertEquals(listOf(25, 30), ages)

实战示例:关联数据

常见于 DTO 组装场景:

val posts = fetchPosts()
val authors = posts.map { authorService.getAuthor(it.authorId) }

val result = authors.zip(posts)
    .map { (author, post) ->
        "《${post.title}》 by ${author.name}"
    }

6. 转换为 Map

除了转成同类集合,我们还可以将任意集合转为 Map 结构。

直接转换:toMap

适用于已有 Pair 列表的情况:

val input = listOf(Pair("one", 1), Pair("two", 2))
val map = input.toMap()
assertEquals(mapOf("one" to 1, "two" to 2), map)

关联生成:associateXXX

根据原集合元素动态生成 key 或 value:

方法 说明
associateBy { } 元素作为 value,lambda 返回 key
associateWith { } 元素作为 key,lambda 返回 value
associate { } 自定义返回 Pair<K,V>

示例:

val inputs = listOf("Hi", "there")

// 元素作 key,长度作 value
val map1 = inputs.associateWith { it.length } // {"Hi" -> 2, "there" -> 5}

// 元素作 value,长度作 key
val map2 = inputs.associateBy { it.length }   // {2 -> "Hi", 5 -> "there"}

// 自定义 kv 对
val map3 = inputs.associate { e -> 
    Pair(e.toUpperCase(), e.reversed()) 
} // {"HI" -> "iH", "THERE" -> "ereht"}

处理重复 Key:groupBy

⚠️ associateBy 遇到重复 key 会覆盖旧值。若要保留所有匹配项,应使用 groupBy

val inputs = listOf("one", "two", "three")
val grouped = inputs.groupBy { it.length }
// 结果:{3=["one","two"], 5=["three"]}

✅ 返回 Map<Int, List<String>>,适合统计、分类等场景。


7. 集合拼接为字符串

有时不需要其他集合结构,而是直接拼成一个字符串输出。

基础拼接:joinToString

val inputs = listOf("Jan", "Feb", "Mar", "Apr", "May")

val simple = inputs.joinToString()
// "Jan, Feb, Mar, Apr, May"

支持自定义分隔符、前后缀:

val detailed = inputs.joinToString(
    separator = ",",
    prefix = "Months: ",
    postfix = "."
)
// "Months: Jan,Feb,Mar,Apr,May."

限制数量与截断

防止日志打印过多内容:

val limited = inputs.joinToString(limit = 3)
// "Jan, Feb, Mar, ..."

可通过 truncated 参数自定义省略符。

边转换边拼接

val upper = inputs.joinToString(transform = String::toUpperCase)
// "JAN, FEB, MAR, APR, MAY"

✅ 比 .map().joinToString() 更高效,只处理实际参与拼接的元素。

写入 Appendable:joinTo

适用于构建大字符串,避免频繁创建临时对象:

val output = StringBuilder()
output.append("My ").append(inputs.size).append(" elements: ")
inputs.joinTo(output)
assertEquals("My 5 elements: Jan, Feb, Mar, Apr, May", output.toString())

8. 归约操作:Reduce & Fold

最后介绍两个强大的聚合方法,用于将集合归约为单一值。

reduce:基于首元素累积

val inputs = listOf("Jan", "Feb", "Mar", "Apr", "May")
val result = inputs.reduce { acc, next -> "$acc, $next" }
// "Jan, Feb, Mar, Apr, May"

⚠️ 缺陷:

  • ❌ 不支持空集合(抛异常)
  • ❌ 初始值必须是第一个元素,类型受限

fold:指定初始值

更通用的版本,允许自定义初始值和类型:

val totalLength = inputs.fold(0) { acc, str -> acc + str.length }
assertEquals(15, totalLength)

✅ 优势:

  • ✅ 支持空集合
  • ✅ 初始值任意类型(如 "", 0, mutableSetOf() 等)

9. 小结

Kotlin 集合 API 设计精巧,覆盖了绝大多数数据处理场景。关键点回顾:

操作类型 推荐方法
过滤 filter, filterNot, filterNotNull
映射 map, mapNotNull, flatMap
合并 zip, unzip
转 Map associateBy, groupBy, toMap
拼接 joinToString, joinTo
聚合 fold, reduce

✅ 建议优先使用 fold 而非 reduce,避免空集合踩坑。
✅ 链式调用时注意性能,合理使用 xxxTo 系列方法减少中间对象创建。

熟练掌握这些方法,能让 Kotlin 代码更加简洁、安全且富有表现力。


原始标题:Collection Transformations in Kotlin