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
编译器做了如下几件事:
- 定义了一个匿名内部类:
SamConversionsKt$main$thread$1
- 获取该类的单例实例:
SamConversionsKt$main$thread$1.INSTANCE
- 将其强转为
Runnable
- 传给
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 获取。