Kotlin 的高阶函数(Higher-Order Function)是指以函数作为参数,或者返回值为函数的函数。
一、两种基本形式
1. 函数作为参数
kotlin
// 定义:接收一个函数参数
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
// 使用:传入 Lambda
val sum = calculate(3, 4) { x, y -> x + y } // 7
val product = calculate(3, 4) { x, y -> x * y } // 12
2. 函数作为返回值
kotlin
// 定义:返回一个函数
fun multiplier(factor: Int): (Int) -> Int {
return { number -> number * factor }
}
// 使用
val triple = multiplier(3)
println(triple(5)) // 15
二、为什么需要高阶函数?
代码复用与抽象:把变化的行为抽出来,骨架保持不变。
kotlin
// 通用模板:只关心"执行前后"的逻辑
fun measureTime(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
val end = System.currentTimeMillis()
println("耗时:${end - start} ms")
}
// 使用:传入任意逻辑
measureTime {
// 这里放你想测量的代码
Thread.sleep(1000)
}
Kotlin 标准库中的经典高阶函数
| 函数 | 作用 | 示例 |
|---|---|---|
let |
非空执行 + 转换 | str?.let { it.length } |
run |
执行代码块并返回结果 | obj.run { name.length } |
apply |
配置对象并返回自身 | TextView().apply { text = "Hi" } |
also |
副作用操作 | list.also { println(it) } |
with |
对对象批量操作 | with(buffer) { append("a"); append("b") } |
filter / map |
集合操作 | list.filter { it > 0 } |
这些本质上都是内联高阶函数(inline),编译时会展开,没有额外函数调用开销。
三、内联优化:inline 关键字
inline 的本质是编译期代码替换(Code Inlining )。它不是运行时的优化,而是编译器直接把函数体和 Lambda 代码"复制粘贴"到调用处。
当高阶函数配合 Lambda 使用时,每次调用都会产生一个匿名类的对象(带来性能开销)。用 inline 可以消除这个开销:
kotlin
inline fun repeatAction(times: Int, action: (Int) -> Unit) {
for (i in 0 until times) {
action(i)
}
}
// 编译后,Lambda 代码直接嵌入调用处,不生成额外对象
repeatAction(3) { println("第 $it 次") }
注意:inline 适合短小、频繁调用的高阶函数;如果 Lambda 参数被存储(而非立即调用),则不能用 inline。
1. 非 inline 高阶函数的编译结果
kotlin
fun measure(block: () -> Unit) {
println("start")
block()
println("end")
}
fun main() {
measure { println("hello") }
}
编译后(近似 Java 字节码):
java
// 1. 编译器为 Lambda 生成匿名类
class Main$1 implements Function0<Unit> {
public Unit invoke() {
println("hello");
return Unit.INSTANCE;
}
}
// 2. 调用处:创建对象 + 方法调用
void main() {
measure(new Main$1()); // ① 创建函数对象
}
// 3. 高阶函数本身:虚方法调用
void measure(Function0<Unit> block) {
println("start");
block.invoke(); // ② 虚方法调用(invoke)
println("end");
}
开销来源:
- 对象分配:new Main$1() --- Lambda 被编译成匿名类实例
- 方法调用:measure() 本身是一次函数调用
- 虚方法分派:block.invoke() 是接口虚调用,JVM 无法内联优化
- 装箱/捕获开销:如果 Lambda 引用了外部变量,这些变量要封装进匿名类的字段
2. 加了 inline 后的编译结果
kotlin
inline fun measure(block: () -> Unit) {
println("start")
block()
println("end")
}
fun main() {
measure { println("hello") }
}
编译后,main 函数直接变成:
java
void main() {
println("start");
println("hello"); // Lambda 代码直接嵌入,没有 invoke 调用
println("end");
}
3. 消除了哪些具体开销?
| 开销类型 | 非 inline | inline | 说明 |
|---|---|---|---|
| 函数对象创建 | new Function0() |
❌ 无 | 不需要生成匿名类,不分配堆内存 |
| 方法调用 | 调用 measure() |
❌ 无 | 函数体直接展开在调用处 |
| Lambda 调用 | block.invoke() |
❌ 无 | Lambda 代码直接嵌入,无虚方法分派 |
| 变量捕获装箱 | 封装到匿名类字段 | ❌ 无 | 外部变量直接以内联方式使用 |
| 栈帧创建 | 有 | ❌ 无 | 不进入新的函数栈帧 |
4. 直观对比
非 inline 调用链:
main() → measure() → block.invoke() → println()
↑ ↑ ↑
栈帧 栈帧 虚方法分派
inline 调用链:
main() → println("start") → println("hello") → println("end")
↑
只有一个栈帧,全部顺序执行
5. 代价(Trade-off)
inline 不是免费的,它用代码体积换执行效率:
- 每处调用都会复制一份函数体,导致生成的字节码变大(Code Bloat)
- 如果函数体很大或被大量调用,会显著增加包体积
- 因此 Kotlin 官方建议:只给短小、高频的高阶函数加 inline
四、优点
| 优点 | 说明 |
|---|---|
| 代码复用 | 把变化的行为抽成参数,骨架代码复用(如 measureTime、repeatAction) |
| 声明式编程 | 描述"做什么"而非"怎么做",代码更简洁(如 list.filter { it.age > 18 }) |
| 语法简洁 | 配合 Lambda,避免大量接口+实现类的样板代码 |
| 标准库丰富 | let/run/apply/also/with、filter/map/reduce 等全是高阶函数 |
| 内联优化 | inline 关键字可消除 Lambda 对象创建的开销,兼顾简洁与性能 |
五、缺点 & 坑点
| 缺点 | 说明 |
|---|---|
| 性能开销 | 非 inline 的高阶函数,每次调用会创建匿名类对象(Kotlin 1.5+ 对非捕获 Lambda 有优化,但捕获变量时仍有开销) |
| 可读性下降 | Lambda 嵌套过深(如回调地狱)会让代码难读 |
| 调试困难 | 堆栈信息里全是 Lambda$1、$inline$ 之类的符号,断点定位麻烦 |
| 非局部返回陷阱 | Lambda 里直接写 return 会返回到外层函数,容易误用 |
| 类型推断局限 | 复杂泛型 + 高阶函数组合时,编译器推断失败,需要显式声明类型 |
| 与 Java 互操作 | Java 调用 Kotlin 高阶函数时语法不友好(需传入 FunctionN 实例) |
六、常见问题
Q1:什么是高阶函数?和普通函数的区别?
接收函数作为参数,或返回值为函数的函数。普通函数操作的是数据,高阶函数操作的是行为。
Q2:inline 的作用是什么?为什么要内联?
inline 在编译期将函数体和 Lambda 代码直接插入调用处,消除函数调用和匿名对象创建的开销。适合短小、频繁调用的高阶函数。
Q3:inline、noinline、crossinline 的区别?
| 关键字 | 作用 |
|---|---|
inline |
函数内联展开,Lambda 参数默认也内联 |
noinline |
标记某个 Lambda 参数不内联(需要作为对象存储或传递时使用) |
crossinline |
Lambda 禁止非局部返回(用于内联函数中,Lambda 在另一个上下文执行的情况,如线程切换) |
Q4:什么是非局部返回(Non-local return)?
在内联函数 的 Lambda 中直接使用 return,会返回到调用该内联函数的外层函数,而非仅退出 Lambda。这是内联特性带来的特殊行为。
kotlin
fun foo() {
run { // inline 函数
return // 直接返回到 foo(),不是只退出 run!
}
println("不会执行")
}
Q5:let、run、apply、also、with 的区别?
| 函数 | 接收者 | 参数 | 返回值 | 典型用途 |
|---|---|---|---|---|
let |
it |
T.() -> R |
Lambda 结果 | 非空判断 + 转换 |
run |
this |
T.() -> R |
Lambda 结果 | 对象计算并返回结果 |
apply |
this |
T.() -> Unit |
对象自身 | 对象配置(Builder 模式) |
also |
it |
T.() -> Unit |
对象自身 | 副作用(打印日志) |
with |
this |
T.() -> R |
Lambda 结果 | 对同一对象批量操作 |
带 a 的(apply、also)返回自身;带 t 的(let)用 it;带 this 的是扩展接收者。
Q6:高阶函数在编译后是什么样子的?
非内联的高阶函数编译后,Lambda 会被编译成匿名类(实现 FunctionN 接口),捕获的变量作为其成员字段。inline 函数则直接展开代码,不生成额外类。
Q7:Kotlin 的函数类型和 Java 的 SAM 接口有什么区别?
- Kotlin 的函数类型(如 (Int) -> Int)是编译器层面的语法糖,底层是 Function0、Function1 等接口。
- Java 的 SAM(Single Abstract Method)接口需要显式定义接口类型。
- Kotlin 支持自动 SAM 转换(Java 接口只有一个抽象方法时,可用 Lambda 替代),但 Kotlin 接口不支持 SAM 转换(需用函数类型或显式实现)。
Q8: 实现一个类似 measureTime 的高阶函数,要求内联,并支持返回耗时和结果。
kotlin
inline fun <T> measureTimeMillis(block: () -> T): Pair<T, Long> {
val start = System.currentTimeMillis()
val result = block()
val cost = System.currentTimeMillis() - start
return result to cost
}
// 使用
val (data, time) = measureTimeMillis {
fetchDataFromNetwork()
}
七、总结
Kotlin 高阶函数是函数式编程的基石,通过将行为参数化 实现代码复用;配合 inline 消除性能开销,但需注意非局部返回和可读性问题。标准库中的 let/run/apply 等本质上都是内联高阶函数。