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

图5.10 lambda的参数对应方法参数

这是有效的,因为OnClickListener接口只有一个抽象方法。这样的接口叫做函数式接口(functional interfaces),或者叫SAM接口。其中SAM表示单一抽象方法(single abstract method)。Java API导出都是像RunnableCallable这样的函数式接口,以及接口中的方法。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以及如何用它们来定义看起来想内置语法的好用的库函数。

results matching ""

    No results matching ""