内联函数、SAM 转换与嵌套类

本章讲三个与 Kotlin/JVM、性能和 Java 互操作密切相关的主题:

  • inline 函数、noinlinecrossinlinereified
  • 函数式接口与 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 的兼容风险

官方文档明确指出,publicprotected 的 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