为什么 Kotlin 调用 java 时可以使用 Lambda? —— SAM 转换机制的介绍

Android · barryhappy · 于 发布 · 最后由 qinglianzhang回复 · 681 次阅读
2553

1. Kotlin 中的 Lambda 表达式

如果你已经开始使用 Koltin, 或者对它有过一些了解的话,那么一定对这种写法并不陌生了:

// 代码一:Kotlin 代码
view.setOnClickListener{
    println("click")
}

它跟下面这段 Java 代码是等价的:

// 代码二:java 代码
view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        System.out.println("click");
    }
});

和 Java8 一样,Kotlin 是支持 Lambda 表达式的,如代码一所示,就是 Lambda 的一个具体应用。

可见,使用 Lambda 减少了很多冗余,使代码写起来更简洁优雅,读起来也更顺畅自然了。

但是,你有没有想过,为什么 Kotlin 可以这样写,这里为什么可以使用 Lambda 作为 setOnClickListener 的参数?

2. 为什么可以这么写?

在 Kotlin 中,一个 Lambda 就是一个匿名函数。

代码一其实是对下面代码三的简写:

// 代码三:Kotlin 代码
view.setOnClickListener({
    v -> println("click")
})

之所以简写成代码一的样子,是基于这两点特性:

  1. 如果 Lambda 是一个函数的唯一参数,那么调用这个函数时可以省略圆括号
  2. 如果 Lambda 所表示的匿名函数只有一个参数,那么可以省略它的声明以及->符号(默认会用it来给省略的参数名命名)

OK,从代码三的结构中,能够更清晰的看出,这里的 view.setOnClickListener 函数是接收了一个 Lambda 作为参数。而在 Kotlin 中,什么样的函数才能把Lambda(也即另一个函数)作为参数呢?
—— 对,就是高阶函数

什么是高阶函数

高阶函数是将函数用作参数或返回值的函数。

这是 Kotlin 和 Java 的区别之一,java 中并没有高阶函数的支持。当我们在 java 中需要用到类似的概念时,通常的做法是传递一个匿名类作为参数,然后实现其中的某些抽象方法 —— 就比如上面的代码二。

事实上,如果在 Android Studio 中,从 Kotlin 的代码查看 view.setOnClickListener 函数的定义,就会发现,看到的函数签名就是一个高阶函数的定义:

函数签名提示

如上图,所看到函数签名是:

public final fun setOnClickListener(l: ((v:View!)->Unit)!): Unit

当然,因为方法是在 Java 中定义的,所以它也列出了 Java 的声明,是这样:

public void setOnClickListener(OnClickListener l)

我们知道,Kotlin 跟 Java 的很多类型都有差异,所以它们在互相调用的时,会有一个按照对应关系的转换。

对于上面的对 setOnClickListener 方法的转换,别的地方都好理解,比较难懂的是,为什么会把参数类型从 OnClickListener 转换成了 (View) -> Unit

(View) -> Unit 是一个函数类型,它表示这样一个函数:接收1个View类型的参数,返回Unit

什么是函数类型

相对于 Java, Kotlin 中多了一个函数类型的概念,即函数是有类型的,一个函数的类型用它的所有的参数的类型和它的函数值的类型共同表示。
比如 (String, Int) -> Int,表示这样的函数类型:第一个参数是 String ,第二个参数 Int ,返回值也是 Int 类型

正是这个对参数类型的转换,使得 setOnClickListener 方法在 Kotlin 中变成了一个高阶函数,这样正是它之所以能够使用 Lambda 作为参数的原因。

而这种转换,就是我们题目中所说到这篇文章的主角 —— SAM 转换 (Single Abstract Method Conversions)。

3. 什么是 SAM 转换

好吧,说了这么多,终于到正题了。

SAM 转换,即 Single Abstract Method Conversions,就是对于只有单个非默认抽象方法接口的转换 —— 对于符合这个条件的接口(称之为 SAM Type ),在 Kotlin 中可以直接用 Lambda 来表示 —— 当然前提是 Lambda 的所表示函数类型能够跟接口的中方法相匹配。

我们知道, OnClickListener 在 java 中的定义是这样的:

// 代码四:OnClickListener 接口在 java 中的定义
public interface OnClickListener {
    void onClick(View v);
}

—— 可以看到,恰好它就是一个符合条件的 SAM Type,onClick 函数的类型即是 (View) -> Unit。所以,在 Kotlin 中,能够用 Lambda 表达式 { println("click")} 来代替 OnClickListener 作为 setOnClickListener 函数的参数。

插一句: SAM 转换其实并不是 Kotlin 独创的,在 Java8 、Scala 中已经有了这种机制。

4. SAM 转换的歧义消除

SAM 转换的存在,使得我们在 Kotlin 中调用 java 的时候能够更得心应手了,它在大部分的时间都能工作的很好。

当然,也偶尔会有例外,比如,考虑下面的这段代码:

// 代码五
public class TestSAM {
    SamType1 sam1;
    SamType2 sam2;
    public void setSam(SamType1 sam1) {
        this.sam1 = sam1;
    }
    public void setSam(SamType2 sam2) {
        this.sam2 = sam2;
    }

    public interface SamType1 {
        void doSomething(int value);
    }
    public interface SamType2 {
        void doSomething2(int value);
    }
}

这段代码中有如下特征:

  • —— TestSAM 有两个重载的 setSam 方法,
  • —— 并且它们的参数( SamType1、SamType2 )都是 SAM Type 的接口。
  • —— 并且 SamType1 跟 SamType2 的唯一抽象方法的函数类型都是 (Int) -> Unit

o(╯□╰)o

这种情况比较吊诡,但是还有有可能会出现的。这时候,如果在 Kotlin 中直接使用代码一类似的方式,就会报错了:

// 代码六:kotlin中调用,这段代码是编译不过的
TestSAM().setSam { 
    println("dodo")  
}

会提示这里歧义,编译器不知道这个 Lambda 代表是 SamType1 跟 SamType2 中的哪一个接口。

解决的办法就是手动标明 Lambda 需要代替的接口类型,有两种方式可以来标明:

// 代码七: 歧义消除
// 方式一
TestSAM().setSam (SamType1 { println("dodo")  }) 
// 方式二
TestSAM().setSam ({ println("dodo") } as SamType1) 

当然,也还有一种方法是不再使用 SAM 转换的机制,而是直接使用一个 SamType1 的实例作为参数:

// 代码八: 使用一个实现接口的匿名类作为参数
TestSAM().setSam(object : TestSAM.SamType1 {
    override fun doSomething(value: Int) {
        println("dodo")
    }
})

这种方法当然也是可以的,只是跟 Lambda 相比起来,就显得不那么优雅了(这很重要!!!)。

5. SAM 转换的限制

Kotin 中 SAM 转换的限制主要有两点:

5.1 只支持 java

即只适用与 Kotlin 中对 java 的调用,而不支持对 Kotlin 的调用

官方的解释是 Kotlin 本身已经有了函数类型高阶函数等支持,所以不需要了再去转换了。

如果你想使用类似的需要用 Lambda 做参数的操作,应该自己去定义需要指定函数类型的高阶函数。

5.2 只支持接口,不支持抽象类。

这个官方没有多做解释。

我想大概是为了避免混乱吧,毕竟如果支持抽象类的话,需要做强转的地方就太多了。而且抽象类本身是允许有很多逻辑代码在内部的,直接简写成一个 Lambda 的话,如果出了问题去定位错误的难度也加大了很多。

6. 总结

OK,讲完了。
总结起来就是 SAM 转换就是 kotlin 在调用 java 代码时能使用 Lambda 的原因。了解了其原理,能够让我们在写代码更自如,在偶尔出问题的时候也能更好更快地解决。

7. 关于作者

http://www.barryzhang.com
https://github.com/barryhappy
http://www.jianshu.com/users/e4607fd59d0d

另外,希望这篇文章能对你有所帮助。
最近开始整了一个微信公众号,用以分享一些 Android 相关以及不相关的干货。

阿弥陀佛,既然都看到这里了,不如扫码关注一下?
不只Android
不只Android

共收到 2 条回复
2553

感谢 @jixiaohua 寂总给的日报推荐~ 😜

5454 1502809648

厉害了我的哥

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册