1. 概述

在 Kotlin 中,Lambda 表达式 提供了一种更加简洁优雅的方式来表达行为逻辑。

本篇文章将重点介绍 Kotlin 的 Lambda 表达式如何通过 SAM 转换(Single Abstract Method Conversions) 实现与 Java 函数式接口的互操作性,并深入探讨其在字节码层面的工作机制。

2. 函数式接口

在 Java 中,Lambda 表达式的实现依赖于 函数式接口(Functional Interface)。这类接口只包含一个抽象方法。

而 Kotlin 在编译期就拥有真正的函数类型(function types),例如:
(String) -> Int 是一个接收 String 参数并返回 Int 的函数类型。

虽然两者实现机制不同,但 Kotlin 的 Lambda 与 Java 的函数式接口完全兼容

举个例子,java.lang.Thread 类的构造函数接受一个 Runnable 接口作为参数:

public Thread(Runnable target) {
    // omitted
}

在 Java 8 之前,我们只能通过匿名内部类来创建线程:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        // the logic
    }
});

而在 Kotlin 中,我们也可以使用 对象表达式(object expression) 来达到相同效果:

val thread = Thread(object : Runnable {
    override fun run() {
        // the logic
    }
})

不过这样写略显啰嗦,不够简洁。

好消息是:Kotlin 允许我们直接传入 Lambda 表达式

val thread = Thread({
    // the logic
})

由于最后一个参数是 Lambda,还可以进一步简化为:

val thread = Thread {
    // the logic
}

也就是说,即使 Java API 要求的是函数式接口,我们也依然可以传入对应的 Lambda 表达式。底层由 Kotlin 编译器自动完成 SAM 转换。

接下来,我们深入看看它是如何工作的。

3. SAM 转换机制

✅ Lambda 能够转换为函数式接口的关键在于:这些接口只有一个抽象方法 —— 这类接口被称为 SAM 接口(Single Abstract Method)。这种自动转换也称为 SAM 转换(SAM Conversions)

不过,在字节码层面,Kotlin 编译器仍然会生成匿名内部类。比如以下代码:

val thread = Thread {
    // the logic
}

使用 kotlinc 编译后:

$ kotlinc SamConversions.kt

再用 javap 查看字节码:

$ javap -v -p -c SamConversionsKt
// truncated
0: new           #11       // class java/lang/Thread
3: dup
4: getstatic     #17       // Field SamConversionsKt$main$thread$1.INSTANCE:LSamConversionsKt$main$thread$1;
7: checkcast     #19       // class java/lang/Runnable
10: invokespecial #23      // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
13: astore_0
// truncated
InnerClasses:
  static final #13;        // class SamConversionsKt$main$thread$1

编译器做了如下几件事:

  1. 定义了一个匿名内部类:SamConversionsKt$main$thread$1
  2. 获取该类的单例实例:SamConversionsKt$main$thread$1.INSTANCE
  3. 将其强转为 Runnable
  4. 传给 Thread 构造函数

3.1. 对象表达式 vs Lambda

前面提到,我们也可以用对象表达式来替代 Lambda:

Thread(object : Runnable {
    override fun run() {
        // the logic
    }
})

但从字节码来看,每次创建线程都会生成新的匿名类实例:

0: new           #11      // class java/lang/Thread
3: dup
4: new           #13      // class SamConversionsKt$main$thread$1
7: dup
8: invokespecial #16      // Method SamConversionsKt$main$thread$1."<init>":()V
11: checkcast     #18     // class java/lang/Runnable
14: invokespecial #21     // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
17: astore_0

注意第 8 行:调用了构造函数,每次都是新建对象。

而 Lambda 则复用了单例:

4: getstatic #17 // Field SamConversionsKt$main$thread$1.INSTANCE:LSamConversionsKt$main$thread$1;

结论:Lambda 不仅更简洁,而且内存效率更高,避免了重复创建对象。

3.2. 闭包带来的性能影响

但如果 Lambda 捕获了外部变量:

var answer = 42
val thread = Thread {
    println(answer)
}

此时字节码发生了变化:

23: invokespecial #25   // Method SamConversionsKt$main$thread$1."<init>":(Lkotlin/jvm/internal/Ref$IntRef;)V
26: checkcast     #27   // class java/lang/Runnable

Kotlin 编译器会把捕获的变量封装进 IntRef 并传递给构造函数。

⚠️ 这意味着每次调用都会创建新实例,失去了之前的性能优势。

解决方案:使用 inline 函数(内联函数)可避免这个问题。

4. SAM 构造器

大多数情况下,Lambda 到函数式接口的转换是自动完成的。但有时我们需要显式指定。

比如 ExecutorService.submit() 有两个重载方法:

Future<T> submit(Callable task);
Future<?> submit(Runnable task);

两个都是函数式接口。如果我们这样写:

val result = executor.submit {
    return@submit 42
}

❌ 编译器无法判断调用的是哪个版本。

✅ 此时可以使用 SAM 构造器(SAM Constructor) 显式指定:

val submit = executor.submit(Callable {
    return@Callable 42
})

SAM 构造器的名字就是函数式接口名本身,它是一个特殊的编译器生成函数,用于显式将 Lambda 转换为接口实例。

此外,SAM 构造器还适用于返回值或变量赋值场景:

fun doSomething(): Runnable = Runnable {
    // doing something
}

val runnable = Runnable { 
    // doing something
}

⚠️ 注意:SAM 转换仅适用于函数式接口,不适用于抽象类,即使该抽象类只有一个抽象方法。

4.1. Kotlin 接口中的 SAM 转换支持

✅ 自 Kotlin 1.4 起,也支持对 Kotlin 接口进行 SAM 转换。

只需在接口前加上 fun 修饰符:

fun interface Predicate<T> {
    fun accept(element: T): Boolean
}

即可使用 Lambda 转换:

val isAnswer = Predicate<Int> { i -> i == 42 }

⚠️ 使用 fun 修饰符时必须确保接口只有一个抽象方法,否则编译报错:

fun interface NotSam {
    // no abstract methods
}

错误信息如下:

Fun interfaces must have exactly one abstract method

5. 总结

本文我们深入了解了 SAM 接口及其在 Kotlin 中的转换机制。借助 SAM 转换,我们可以轻松地将 Kotlin Lambda 传递给需要函数式接口的 Java API。

同时我们也看到了:

  • Lambda 相比对象表达式更加高效(复用单例)
  • 若捕获变量则可能失去性能优势
  • 在重载方法中需显式使用 SAM 构造器
  • Kotlin 1.4 后也支持 Kotlin 接口的 SAM 转换

所有示例代码可在 GitHub 获取。


原始标题:SAM Conversions in Kotlin