kotlin与Kotlin库一起使用lambda是非常棒的。但是你用到的大部分API可能是用Java编写的,而不是Kotlin。好消息是,Kotlin的lambda可以完全和Java API进行互操作。在这一章节,你将会完全看到这一点是如何实现的。 在这一章的开头,你看到了向Java方法传递lambda的例子:
button.setOnClickListener { /* actions on click */ } // 将lambda当做一个参数进行传递
Button
类通过接收一个OnClickListener
类型的setOnClickListener
方法来为按钮设置了一个新的侦听器:
/* Java */
public class Button {
public void setOnClickListener(OnClickListener l) { ... }
}
OnClickListener
接口声明了一个onClick
方法:
/* Java */
public interface OnClickListener {
void onClick(View v);
}
Java 8之前的版本中,你必须创建一个匿名类的实例并把它当做一个参数传递给setOnClickListener
方法:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
...
}
}
在Kotlin中,你可以传递lambda:
button.setOnClickListener { view -> ... }
用来实现OnClickListener
的lambda有一个View
类型的参数,和在onClick
方法中的一样。图5.10解释了这种对应关系。
图5.10 lambda的参数对应方法参数
这是有效的,因为OnClickListener
接口只有一个抽象方法。这样的接口叫做函数式接口(functional interfaces),或者叫SAM接口。其中SAM表示单一抽象方法(single abstract method)。Java API导出都是像Runnable
和Callable
这样的函数式接口,以及接口中的方法。Kotlin允许你在调用接收函数式接口作为参数的Java方法时使用lambda。这确保你的Kotlin代码保持整洁和地道。
NOTE 注意 不同于Java,Kotlin拥有真正的函数类型。因此,需要接收lambda作为参数的Kotlin方法应当使用函数类型,而不是函数式接口类型作为这些参数的类型。lambda到实现了Kotlin接口的对象之间的自动转换是不支持的。我们将会在8.1.1节讨论在方法声明中使用函数类型
我们来详细看看当你把lambda传递给一个接收函数式接口类型作为参数的方法时会发生什么。
5.4.1 把lambda当做参数传递给Java方法
你可以把lambda传递给任何一个需要一个函数接口的Java方法。例如,思考一下这个接收lambda作为参数的方法:
/* Java */
void postponeComputation(int delay, Runnable computation);
在Kotlin中,你可以调用它并向它传递一个作为参数的lambda。编译器将会自动把它转换为一个Runnable
的实例:
postponeComputation(1000) { println(42) }
要注意的是,当我们说“一个Runnable
的实例”时,我们的意思是“一个实现了Runnable
接口的匿名类实例”。编译器会为你创建这个实例,并且使用lambda作为唯一的抽象方法--在这个案例中,run
方法的主体。
你可以通过创建显式实现Runnable
接口的匿名对象来达到这个效果:
postponeComputation(1000, object : Runnable // 当你传递一个lambda时,(编译器)会创建匿名类
{ override fun run() {
println(42)
}
})
但是这儿有点不同。当你显式声明一个对象时,每次调用都会创建一个新的实例。使用lambda,情况就不一样了:如果lambda没有访问定义它的函数的变量,对应的匿名类实例将会在调用之间进行重用:
postponeComputation(1000) { println(42) } // 为整个程序创建一个`Runnable`实例
因此,下面的代码跟使用object
显式声明是等价的实现。它把Runnable
实例保存在一个变量中,并在每一次调用中使用:
val runnable = Runnable { println(42) } // 编译为一个静态变量。程序中的仅有的一个实例。
fun handleComputation() {
postponeComputation(1000, runnable) // 2 每次`handleComputation`调用都使用同一个对象。
}
如果lambda捕捉来自上下文的变量,那就不再可能为每次调用重用同一个实例了。在这种情况下,编译器会为每次调用创建一个新的对象并保存在该对象中捕获的值。举个例子,下面的函数中,每次调用都使用了一个新的Runnable
实例,并将id
值保存为一个字段:
fun handleComputation(id: String) // 在lambda中捕捉“id”变量
{ postponeComputation(1000) // 在每次handleComputation调用中创建一个新的Runnable实例
{ println(id) }
SIDBAR lambda的实现细节 从Kotlin1.0开始,每个lambda表达式都会被编译为匿名类,除非它是一个内联lambda。后续的Kotlin版本将会支持生成Java 8字节码。这将会让编译器避免为每个lambda表达式创建一个单独的.class文件。 如果lambda进行变量捕捉,匿名类将为每个捕获变量准备一个字段,同时会为每个调用创建这个类的一个新的实例。否则,(编译器)将会创建一个单例。匿名类的名字通过定义lambda的函数的名字添加前缀进行派生,例如:
HandleComputation$1
。 如果你反编译了之前的lambda表达式的字节码,你将会看到:
class HandleComputation$1(val id: String) : Runnable { override fun run() { println(id) } } fun handleComputation(id: String) { postponeComputation(1000, HandleComputation$1(id)) // 底层实现不是一个lambda,而是创建了一个特殊类的实例 }
如你所见,编译器为每一个捕获的变量生成一个字段和构造函数参数。
注意了,关于创建匿名类和lambda的类实例的讨论对于函数式接口以外的Java方法是有效的。但是对于使用Kotlin扩展函数的集合是无效的。如果你向标记为inline
的Kotlin函数传递lambda,编译器不会创建匿名类。大部分的库函数都被标记为inline
。有关其工作原理的细节,我们会在后续的8.2章节讨论。
如你所见,大部分情况下,lambda会自动转换为函数式接口,而无需其他的工作。但是,当你需要显式执行转换时,情况有所不同。我们来看看要如何做。
5.4.2 SAM构造函数:lambda变换函数式接口的显式的转换
SAM构造函数是一个由编译器生成的函数。它能让你显式的把lambda转换为函数式接口。当编译器没有自动进行转换时,你可以使用它。举个例子,假如你有一个返回函数式接口的方法,你不能直接返回一个lambda。你必须把它包装成SAM构造函数。下面有一个简单的例子:
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All done!") }
}
>>> createAllDoneRunnable().run()
All done!
SAM构造函数的名字跟底层的函数式接口的名字一样。SAM构造函数只接收一个参数--lambda。这个lambda将被用作函数式接口中的单一抽象方法的主体。同时,它还会返回实现了Runnable
接口的类实例。
为了返回一个值,当你需要把从lambda生成的函数式接口的实例保存在一个变量时,你会用到SAM构造函数。假定在下面的例子中,你想要为多个按钮重用一个侦听器(在Android应用中,这份代码会是Activity.onCreate
方法的一部分):
val listener = OnClickListener { view ->
val text = when (view.id) { // 1 使用view.id来判断那个按钮被点击了
R.id.button1 -> "First button"
R.id.button2 -> "Second button"
else -> "Unknown button"
}
toast(text) // 2 向用户展示“text”变量的内容
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)
listener
检查那个按钮是点击的来源并做出相应的行为。你可以通过使用实现了OnClickListener
的对象声明来创建侦听器,但是SAM构造函数给了你一个更精简的选项。
SIDEBAR lambda和添加/移除侦听器 注意,由于处在匿名对象内部,lambda中没有
this
关键字:无法在被转换的lambbda中引用匿名类实例。从编译器的角度来看,lambda是一块代码,不是一个对象。你不能把它引用为一个对象。lambda中的this
引用指向包围它的类。 如果你的事件侦听器在处理事件时,需要将自身取消订阅,你无法为此使用lambda。相反的,你应该使用一个匿名对象来实现侦听器。在匿名对象中,this
关键字指向该对象的实例。你可以把它传递给移除侦听器的API。
还有,尽管SAM在方法转换时转换通常会自动执行,但是也有例外。在你把lambda当做参数传递给一个重载函数时,编译器无法选择正确的重载函数。在这种情况下,应用SAM构造函数是解决这个编译器错误的好方法。 为了完成我们对lambda语法和用法的讨论,我们来看看有接收器的lambda以及如何用它们来定义看起来想内置语法的好用的库函数。