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

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 ?

2. 为什么可以这么写?

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

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

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

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

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

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

什么是高阶函数?

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

这是 Kotlin 和 Java 的区别之一,java 中并没有高阶函数的支持(java8是有高阶函数的)。当我们在 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

正是这个对参数类型的转换,使得 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 函数的参数。

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 转换的限制

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容