1. 概述
在使用 Kotlin 的 List 时,我们经常需要对列表中的每个元素执行某种操作。Kotlin 提供了一组强大的高阶函数,可以高效地对列表元素应用函数。
本文将深入探讨实现这一常见任务的多种方法和技巧。
2. 问题背景
对列表元素应用函数通常涉及两种典型场景:
✅ 转换元素(Transformation)
我们希望将原列表中的每个元素通过某个函数映射为新值。例如:
- 将
Int
列表转为String
列表(调用toString()
) - 将 ID 列表通过数据库查询转为对应的实体对象
在这种情况下,每个元素作为函数的输入参数,函数的返回值即为转换后的结果元素。
❌ 执行动作(Side Effect)
我们只是想对每个元素执行某个操作,但并不关心函数的返回值。例如:
- 给一批手机号发送短信
- 将每个元素打印到控制台作为日志输出
本文将分别讨论这两种场景,并介绍对应的解决方案。但在深入之前,先准备两个示例扩展函数:
fun String.reverseCase() = map { c -> if (c.isLowerCase()) c.uppercase() else c.lowercase() }.joinToString(separator = "")
fun String.printCaseReversed() = println("$this -> ${reverseCase()}")
如上所示,我们定义了两个 String 的扩展函数:
reverseCase()
:反转字符串中每个字符的大小写并返回新字符串printCaseReversed()
:内部调用reverseCase()
并打印结果,无返回值(返回Unit
)
接下来初始化测试数据:
val myList = listOf("a a a", "B B B", "c C c", "D d D", "e E E", "F F f")
若对 myList
中每个元素应用 reverseCase()
,预期结果应为:
val expected = listOf("A A A", "b b b", "C c C", "d D d", "E e e", "f f F")
为验证正确性,后续示例将使用单元测试断言进行校验。
3. 使用 map() 函数
当涉及列表转换时,map()
是最常用的方法。其核心行为是:
🔁 遍历列表,将每个元素 A 映射为新值 B,最终生成一个包含所有 B 的新列表
下面用 map()
对 myList
应用 reverseCase()
:
val result = myList.map { it.reverseCase() }
assertEquals(expected, result)
关键点:
- ✅ 返回一个全新的列表对象(不可变)
- ❌ 不修改原始列表
- ⚠️ 原始列表
myList
必须是只读的(List<T>
),否则可能引发并发修改问题
这是函数式编程中最推荐的“无副作用”处理方式。
4. 在 MutableList 上使用 replaceAll()
如果目标是就地修改(in-place mutation)列表本身(而非创建新列表),且列表类型为 MutableList
,可使用 replaceAll()
:
val mutableList = myList.toMutableList()
assertEquals(myList, mutableList)
mutableList.replaceAll { it.reverseCase() }
assertEquals(expected, mutableList)
注意要点:
- ✅ 真正实现了原地替换,内存更友好
- ✅ 语法与
map()
高度相似,学习成本低 - ⚠️ 仅适用于
MutableList
,对只读List
调用会抛出异常 - ⚠️ 破坏了不可变性原则,在多线程或函数式风格代码中需谨慎使用
这个方法适合性能敏感且确定无共享引用的场景。
5. 使用 forEach() 函数
当我们只关心执行动作(如打印、发消息),而不关心返回值时,forEach()
是直观选择:
myList.forEach { it.printCaseReversed() } // 输出每个元素处理结果
输出如下:
a a a -> A A A
B B B -> b b b
c C c -> C c C
D d D -> d D d
e E E -> E e e
F F f -> f f F
也可用于填充另一个集合:
val result = mutableListOf<String>()
myList.forEach { result += it.reverseCase() }
assertEquals(expected, result)
⚠️ 但必须注意:forEach()
的函数签名决定了它的局限性:
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit
这意味着:
- ❌ 返回值为
Unit
(类似 Java 的void
) - ❌ 无法链式调用后续操作(它是终端操作)
- ❌ 在流式处理中会中断管道
这在某些复杂逻辑中会带来不便——比如你想先打印日志,再做转换。
6. 使用 onEach() 函数
为解决 forEach()
不能链式调用的问题,Kotlin 提供了 onEach()
:
public inline fun <T, C : Iterable<T>> C.onEach(action: (T) -> Unit): C {
return apply { for (element in this) action(element) }
}
对比 forEach()
和 onEach()
:
| 方法 | 返回类型 | 是否支持链式调用 |
|------|--------|----------------|
| forEach()
| Unit
| ❌ |
| onEach()
| 原集合本身 (C
) | ✅ |
示例:
val result = myList.onEach { it.printCaseReversed() }
assertSame(myList, result) // 返回的是原列表
输出与 forEach()
相同:
a a a -> A A A
B B B -> b b b
c C c -> C c C
D d D -> d D d
e E E -> E e e
F F f -> f f F
✅ 最大优势:支持链式操作!
val resultList = myList.onEach { it.printCaseReversed() }
.map { it.uppercase().replace(" ", ", ") }
assertEquals(listOf("A, A, A", "B, B, B", "C, C, C", "D, D, D", "E, E, E", "F, F, F"), resultList)
💡 踩坑提示:很多人误以为
onEach()
是forEach()
的替代品,其实它更适合作为“中间调试”工具——比如你在.filter().map().sorted()
流程中想插入日志,用onEach()
再合适不过。
7. 总结
场景 | 推荐方法 | 特点 |
---|---|---|
转换元素 → 新列表 | map() |
函数式首选,安全不可变 |
就地修改可变列表 | replaceAll() |
内存高效,注意线程安全 |
执行副作用动作 | forEach() |
简单直接,但终结流 |
中间插入副作用 | onEach() |
支持链式调用,调试利器 |
选择建议:
- 默认优先使用
map()
+ 不可变列表 - 性能关键且确定上下文安全时用
replaceAll()
- 日志/监控等中间操作务必用
onEach()
避免破坏流式处理
完整示例代码详见 GitHub 仓库