内联函数、SAM 转换与嵌套类¶
本章讲三个与 Kotlin/JVM、性能和 Java 互操作密切相关的主题:
inline函数、noinline、crossinline、reified。- 函数式接口与 SAM 转换。
- 嵌套类、内部类、匿名对象和 Java listener 风格互操作。
这些内容经常出现在标准库、框架 DSL、Spring/Ktor 扩展 API、Android 回调、测试工具和库作者代码中。对 Java 开发者来说,难点不是语法,而是 Kotlin 在编译期做了哪些转换。
为什么需要 inline¶
高阶函数会接收函数作为参数:
fun measure(block: () -> Unit) {
val start = System.nanoTime()
block()
println(System.nanoTime() - start)
}
调用:
measure {
println("work")
}
普通高阶函数可能产生函数对象、闭包捕获和虚调用。官方 inline 文档说明,这些开销在很多场景下可以通过内联 lambda 消除。
把函数标记为 inline:
inline fun measure(block: () -> Unit) {
val start = System.nanoTime()
block()
println(System.nanoTime() - start)
}
编译器会把函数体和可内联 lambda 展开到调用点。概念上类似:
val start = System.nanoTime()
println("work")
println(System.nanoTime() - start)
Java 对比:Java 没有 Kotlin 这种源码级 inline 修饰符。JIT 运行时也会做方法内联优化,但 Kotlin inline 是编译器参与 API 语义的一部分,特别影响非局部返回和 reified 泛型。
inline 的适用场景¶
适合使用 inline:
- 高频调用的小型高阶函数。
- 需要
reified类型参数。 - 需要允许 lambda 非局部返回。
- DSL 构建函数,例如资源管理、锁、事务、测量、集合操作。
不适合使用 inline:
- 函数体很大,展开后导致字节码膨胀。
- 没有函数类型参数,也没有 reified 类型参数。
- 只是猜测“inline 一定更快”。
- 公开库 API 中没有认真评估二进制兼容。
官方文档提到,如果 inline 函数没有可内联函数参数,也没有 reified 类型参数,编译器会给出警告,因为这种内联通常收益不大。
noinline¶
inline 函数中的函数参数默认都会被内联。如果某个 lambda 需要被保存、传递或作为对象使用,就不能内联,可以标记 noinline:
inline fun runBoth(
first: () -> Unit,
noinline second: () -> Unit,
) {
first()
val later = second
later()
}
first 会内联,second 不会内联。
为什么需要 noinline?因为被内联的 lambda 不再是一个普通函数对象,不能随意存进变量、字段或传给需要函数对象的地方。
crossinline¶
inline lambda 可以非局部返回:
inline fun runNow(block: () -> Unit) {
block()
}
fun demo() {
runNow {
return
}
}
这里 return 返回的是 demo()。
但如果 inline 函数把 lambda 放到另一个执行上下文里,例如对象或嵌套函数中,就不能允许这种非局部返回:
inline fun runLater(crossinline block: () -> Unit): Runnable {
return Runnable {
block()
}
}
crossinline 表示:这个 lambda 仍然可以内联,但不允许非局部返回。
Java 对比:Java lambda 中 return 从 lambda 返回,不会从外层方法返回。Kotlin 因为 inline 支持非局部返回,所以才需要 crossinline 这种限制。
非局部返回¶
标准库 forEach 是 inline 函数,因此可以这样写:
fun hasNegative(values: List<Int>): Boolean {
values.forEach {
if (it < 0) return true
}
return false
}
这段代码中的 return true 不是返回 lambda,而是返回 hasNegative。
这很强,但要克制。复杂控制流建议写普通循环:
fun hasNegative(values: List<Int>): Boolean {
for (value in values) {
if (value < 0) return true
}
return false
}
如果你只想跳过当前元素,用标签:
values.forEach {
if (it < 0) return@forEach
println(it)
}
reified 类型参数¶
Java 和 Kotlin/JVM 泛型通常会类型擦除:
fun <T> Any?.isType(): Boolean {
// return this is T // 普通泛型中不允许
return false
}
inline 函数可以使用 reified,让类型参数在调用点可用:
inline fun <reified T> Any?.isType(): Boolean =
this is T
调用:
println("text".isType<String>()) // true
println(42.isType<String>()) // false
常见用途:
inline fun <reified T> List<*>.filterIs(): List<T> =
filterIsInstance<T>()
或者封装需要 Class<T> 的 Java API:
inline fun <reified T : Any> jsonToObject(json: String): T =
objectMapper.readValue(json, T::class.java)
Java 对比:Java 通常要传 Class<T>:
User user = mapper.readValue(json, User.class);
Kotlin reified 可以让调用点更简洁:
val user: User = jsonToObject(json)
但只有 inline 函数能使用 reified。普通函数不能在运行期直接知道 T。
inline 属性¶
没有 backing field 的属性访问器也可以 inline:
val nowNanos: Long
inline get() = System.nanoTime()
也可以标记整个属性:
inline val String.firstOrNullSafe: Char?
get() = firstOrNull()
这种写法通常用于非常轻量的计算属性。不要把复杂逻辑伪装成属性,也不要为了少一个函数调用随意 inline。
公开 inline API 的兼容风险¶
官方文档明确指出,public 或 protected 的 inline 函数属于模块公开 API,并且会在其他模块调用点内联。这会带来二进制兼容风险。
例如:
public inline fun validate(value: String): Boolean =
value.isNotBlank() && internalCheck(value)
公开 inline 函数不能随意调用非公开声明。因为调用方编译时需要把函数体展开过去,内部实现就会影响调用方产物。
如果确实需要让公开 inline 函数调用 internal 成员,可以使用 @PublishedApi:
@PublishedApi
internal fun internalCheck(value: String): Boolean =
value.length <= 64
public inline fun validate(value: String): Boolean =
value.isNotBlank() && internalCheck(value)
库作者要把 @PublishedApi internal 当作接近公开 API 的契约处理,不能随意删除或改签名。
函数式接口¶
只有一个抽象成员函数的接口叫函数式接口,也叫 SAM 接口。Kotlin 用 fun interface 声明:
fun interface IntPredicate {
fun accept(value: Int): Boolean
}
可以用对象表达式实现:
val even = object : IntPredicate {
override fun accept(value: Int): Boolean = value % 2 == 0
}
也可以用 SAM 转换:
val even = IntPredicate { it % 2 == 0 }
Java 对比:
@FunctionalInterface
interface IntPredicate {
boolean accept(int value);
}
IntPredicate even = value -> value % 2 == 0;
Kotlin 的 fun interface 和 Java 的 @FunctionalInterface 目标类似:把“一个抽象方法的接口”变成 lambda 友好的 API。
SAM 转换¶
SAM 转换会把签名匹配的 lambda 转成函数式接口实例:
fun runTask(task: Runnable) {
task.run()
}
runTask {
println("running")
}
这里 Runnable 是 Java 函数式接口,Kotlin 可以直接传 lambda。
自定义 Kotlin 函数式接口也可以:
fun interface ClickHandler {
fun onClick(x: Int, y: Int)
}
fun register(handler: ClickHandler) {
handler.onClick(10, 20)
}
register { x, y ->
println("clicked at $x,$y")
}
如果函数参数类型本来就是函数类型:
fun register(handler: (Int, Int) -> Unit) {
handler(10, 20)
}
则不需要 SAM 接口。
fun interface vs typealias¶
两者看起来都能给函数形状起名:
fun interface IntPredicate {
fun accept(value: Int): Boolean
}
typealias IntPredicateAlias = (Int) -> Boolean
区别很重要:
| 维度 | fun interface | typealias 函数类型 |
|---|---|---|
| 是否创建新类型 | 是 | 否,只是别名 |
| 可否有成员 | 可有非抽象成员 | 不可 |
| 可否继承接口 | 可以 | 不可以 |
| Java 互操作 | 更友好 | Java 侧看到 Kotlin 函数类型 |
| 语义强度 | 强,可以表达领域概念 | 弱,只缩短函数类型 |
选择建议:
- API 只是接收一个简单函数:用函数类型或
typealias。 - API 需要表达明确领域概念、扩展成员、Java 友好调用或未来演进空间:用
fun interface。
示例:
typealias Mapper<T, R> = (T) -> R
适合简单函数形状。
fun interface RetryPolicy {
fun shouldRetry(attempt: Int, error: Throwable): Boolean
fun maxAttempts(): Int = 3
}
适合有契约和默认行为的领域接口。
从构造函数工厂迁移到 fun interface¶
旧代码可能这样写:
interface Printer {
fun print()
}
fun Printer(block: () -> Unit): Printer =
object : Printer {
override fun print() = block()
}
新代码可以写成:
fun interface Printer {
fun print()
}
Kotlin 官方文档提到,从 Kotlin 1.6.20 起,函数式接口构造器的 callable reference 支持可以帮助迁移。为了保持二进制兼容,可以把旧工厂函数标记为隐藏废弃:
@Deprecated(
message = "Use fun interface constructor instead.",
level = DeprecationLevel.HIDDEN,
)
fun Printer(block: () -> Unit): Printer =
object : Printer {
override fun print() = block()
}
库作者做这类迁移时必须跑二进制兼容检查,不能只看源码能否编译。
嵌套类¶
Kotlin 类可以嵌套在另一个类中:
class Outer {
private val value = 1
class Nested {
fun answer(): Int = 42
}
}
fun main() {
val result = Outer.Nested().answer()
println(result)
}
注意:Kotlin 的嵌套类默认不持有外部类实例引用。因此 Nested 不能直接访问 Outer.value。
Java 对比:
- Java 的非静态内部类默认持有外部实例。
- Java 的
static class Nested更接近 Kotlin 默认嵌套类。 - Kotlin 如果需要持有外部实例,必须显式写
inner。
这点和 Java 习惯正好相反。Java 开发者很容易以为嵌套类天然能访问外部对象,在 Kotlin 中不是这样。
inner 内部类¶
使用 inner 后,内部类会持有外部对象引用,并可访问外部成员:
class Outer {
private val value = 1
inner class Inner {
fun answer(): Int = value
}
}
fun main() {
val result = Outer().Inner().answer()
println(result)
}
内部类创建方式也体现了外部实例依赖:
val outer = Outer()
val inner = outer.Inner()
不要滥用 inner。它会让内部对象持有外部对象引用,可能延长生命周期,在 Android、桌面 UI 或服务端缓存中都可能导致内存问题。
嵌套接口和类¶
Kotlin 允许类和接口互相嵌套:
interface Parser {
class Result(val value: String)
interface ErrorHandler {
fun handle(error: Throwable)
}
}
class JsonParser {
interface Listener {
fun onParsed(value: String)
}
}
适合把只服务某个外部类型的辅助类型收拢到命名空间下。但如果嵌套太深,会降低可读性,尤其是公开 API。
匿名对象¶
Kotlin 用 object expression 创建匿名对象:
val listener = object : MouseAdapter() {
override fun mouseClicked(event: MouseEvent) {
println("clicked")
}
}
这类似 Java 的匿名内部类:
MouseAdapter listener = new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent event) {
System.out.println("clicked");
}
};
如果目标类型是 Java 函数式接口,可以用 SAM 转换:
val listener = java.awt.event.ActionListener {
println("clicked")
}
选择规则:
- 只实现一个 Java SAM 方法:优先 lambda。
- 需要重写多个方法或持有状态:用
object : Type { ... }。 - 需要命名和复用:写具名类。
对象表达式与 this¶
匿名对象内部的 this 指匿名对象本身:
class Screen {
fun bind(button: Button) {
button.onClick(object : ClickListener {
override fun clicked() {
println(this) // ClickListener 匿名对象
println(this@Screen) // 外层 Screen
}
})
}
}
在 Java 中,匿名内部类里也常用 OuterClass.this 访问外层对象。Kotlin 对应写法是 this@Screen。
API 设计建议¶
接收回调时:函数类型还是 fun interface¶
简单回调:
fun onComplete(callback: (Result) -> Unit)
领域语义更强、需要 Java 友好:
fun interface CompletionListener {
fun onComplete(result: Result)
}
fun addCompletionListener(listener: CompletionListener)
如果公开库主要给 Kotlin 用户,用函数类型很简洁。如果也服务 Java 用户,fun interface 往往更清晰。
不要为所有高阶函数加 inline¶
差的写法:
inline fun log(message: () -> String) {
println(message())
}
这可能有用,但也可能只是过度优化。更值得 inline 的情况是:
inline fun <T> lock(lock: Lock, body: () -> T): T {
lock.lock()
try {
return body()
} finally {
lock.unlock()
}
}
这里 inline 能减少 lambda 开销,并允许调用方在 lambda 中自然返回。
公开 inline 函数要少而稳¶
库 API 中的 inline 函数一旦发布,演进成本比普通函数高。建议:
- 写显式返回类型。
- 不暴露大型实现。
- 不依赖易变 internal 实现,必要时
@PublishedApi。 - 修改前跑 binary compatibility validator。
- 文档说明非局部返回、异常、线程或资源管理语义。
常见误区¶
误区一:inline 等于性能一定更好¶
inline 消除一部分调用和 lambda 对象开销,但也可能导致字节码膨胀、编译变慢、指令缓存压力变大。小型高频高阶函数更适合 inline。
误区二:reified 能解决所有泛型擦除¶
reified 只在 inline 函数调用点可用。它不能让普通泛型类在运行期保留完整嵌套泛型信息,也不能绕过所有类型擦除限制。
误区三:Kotlin 嵌套类等于 Java 内部类¶
Kotlin 默认嵌套类更像 Java static nested class。需要外部实例引用时必须写 inner。
误区四:typealias 和 fun interface 只是两种写法¶
typealias 不创建新类型,fun interface 创建新接口类型。公开 API 中二者语义和兼容性不同。
误区五:SAM 转换只适用于 Java¶
Kotlin 的 fun interface 也支持 SAM 转换。Java 函数式接口和 Kotlin 函数式接口都可以用 lambda 创建实例。
官方参考¶
- Inline functions:https://kotlinlang.org/docs/inline-functions.html
- Functional interfaces:https://kotlinlang.org/docs/fun-interfaces.html
- Nested and inner classes:https://kotlinlang.org/docs/nested-classes.html