1-3 Lambda 是什么?
Lambda 可以理解为函数的简写
。
fun onClick(v: View): Unit { … }
setOnClickListener(::onClick)
// 用 Lambda 表达式来替代函数引用
setOnClickListener({v: View -> …})
看到这,如果你没有疑惑,那恭喜你,这说明你的悟性很高,或者说你基础很好;如果你感觉有点懵,那也很正常,请看后面详细的解释。
2. 为什么要引入 Lambda 和 高阶函数?
刚接触到高阶函数和 Lambda 的时候,我就一直有个疑问:为什么要引入 Lambda 和 高阶函数?这个问题,官方文档里没有解答,因此我只能自己去寻找。
2-1 Lambda 和 高阶函数解决了什么问题?
这个问题站在语言的设计者角度会更明了,让我们看个实际的例子,这是 Android 中的 View 定义,我省略了大部分代码:
// View.java
private OnClickListener mOnClickListener;
private OnContextClickListener mOnContextClickListener;
// 监听手指点击事件
public void setOnClickListener(OnClickListener l) {
mOnClickListener = l;
}
// 为传递这个点击事件,专门定义了一个接口
public interface OnClickListener {
void onClick(View v);
}
// 监听鼠标点击事件
public void setOnContextClickListener(OnContextClickListener l) {
getListenerInfo().mOnContextClickListener = l;
}
// 为传递这个鼠标点击事件,专门定义了一个接口
public interface OnContextClickListener {
boolean onContextClick(View v);
}
Android 中设置点击事件和鼠标点击事件,分别是这样写的:
// 设置手指点击事件
image.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
gotoPreview();
}
});
// 设置鼠标点击事件
image.setOnContextClickListener(new View.OnContextClickListener() {
@Override
public void onContextClick(View v) {
gotoPreview();
}
});
请问各位小伙伴有没有觉得这样的代码很啰嗦?
现在我们假装自己是语言设计者,让我们先看看上面的代码存在哪些问题:
-
定义方:每增加一个方法,就要新增一个接口:
OnClickListener
,OnContextClickListener
-
调用方:需要写一堆的匿名内部类,啰嗦,繁琐,毫无重点
仔细看上面的代码,开发者关心的其实只有一行代码:
gotoPreview();
如果将其中的核心逻辑抽出来,这样子才是最简明的:
image.setOnClickListener { gotoPreview() }
image.setOnContextClickListener { gotoPreview() }
Kotlin 语言的设计者是怎么做的?是这样:
-
用函数类型替代接口定义
-
用 Lambda 表达式作为函数参数
与上面 View.java 的等价 Kotlin 代码如下:
//View.kt
var mOnClickListener: ((View) -> Unit)? = null
var mOnContextClickListener: ((View) -> Unit)? = null
fun setOnClickListener(l: (View) -> Unit) {
mOnClickListener = l;
}
fun setOnContextClickListener(l: (View) -> Unit) {
mOnContextClickListener = l;
}
以上做法有以下的好处:
-
定义方:减少了两个接口类的定义
-
调用方:代码更加简明
细心的小伙伴可能已经发现了一个问题:Android 并没有提供 View.java 的 Kotlin 实现,为什么我们 Demo 里面可以用 Lambda 来简化事件监听?
// 在实际开发中,我们经常使用这种简化方式
setOnClickListener { gotoPreview() }
原因是这样的:由于 OnClickListener
符合 SAM 转换
的要求,因此编译器自动帮我们做了一层转换,让我们可以用 Lambda 表达式来简化我们的函数调用。
那么,SAM
又是个什么鬼?
2-2 SAM 转换(Single Abstract Method Conversions)
SAM
(Single Abstract Method),顾名思义,就是:只有一个抽象方法的类或者接口,但在 Kotlin 和 Java8 里,SAM 代表着:只有一个抽象方法的接口。符合 SAM 要求的接口,编译器就能进行 SAM 转换:让我们可以用 Lambda 表达式来简写接口类的参数。
注:Java8 中的 SAM 有明确的名称叫做:函数式接口
(FunctionalInterface)。
FunctionalInterface 的限制如下,缺一不可:
-
必须是接口,抽象类不行
-
该接口有且仅有一个抽象的方法,抽象方法个数必须是1,默认实现的方法可以有多个。
也就是说,对于 View.java 来说,它虽然是 Java 代码,但 Kotlin 编译器知道它的参数 OnClickListener
符合 SAM 转换的条件,所以会自动做以下转换:
转换前:
public void setOnClickListener(OnClickListener l)
转换后:
fun setOnClickListener(l: (View) -> Unit)
// 实际上是这样:
fun setOnClickListener(l: ((View!) -> Unit)?)
((View!) -> Unit)?
代表,这个参数可能为空。
2-3 Lambda 表达式引发的8种写法
当 Lambda 表达式作为函数参数的时候,有些情形下是可以简写的,这时候可以让我们的代码看起来更简洁。然而,大部分初学者对此也比较头疼,同样的代码,能有 8 种不同的写法,确实也挺懵的。
要理解 Lambda 表达式的简写逻辑,其实很简单,那就是:多写
。
各位小伙伴可以跟着我接下来的流程来一起写一写:
2-3-1 第1种写法
这是原始代码,它的本质是用 object 关键字定义了一个匿名内部类
:
image.setOnClickListener(object: View.OnClickListener {
override fun onClick(v: View?) {
gotoPreview(v)
}
})
2-3-2 第2种写法
如果我们删掉 object
关键字,它就是 Lambda 表达式了,因此它里面 override 的方法也要跟着删掉:
image.setOnClickListener(View.OnClickListener { view: View? ->
gotoPreview(view)
})
上面的 View.OnClickListener
被称为: SAM Constructor
—— SAM 构造器,它是编译器为我们生成的。Kotlin 允许我们通过这种方式来定义 Lambda 表达式。
思考题:
这时候,View.OnClickListener {}
在语义上是 Lambda 表达式,但在语法层面还是匿名内部类
。这句话对不对?
2-3-3 第3种写法
由于 Kotlin 的 Lambda 表达式是不需要 SAM Constructor
的,所以它也可以被删掉。
image.setOnClickListener({ view: View? ->
gotoPreview(view)
})
2-3-4 第4种写法
由于 Kotlin 支持类型推导
,所以 View?
可以被删掉:
image.setOnClickListener({ view ->
gotoPreview(view)
})
2-3-5 第5种写法
当 Kotlin Lambda 表达式只有一个参数的时候,它可以被写成 it
。
image.setOnClickListener({ it ->
gotoPreview(it)
})
2-3-6 第6种写法
Kotlin Lambda 的 it
是可以被省略的:
image.setOnClickListener({
gotoPreview(it)
})
2-3-7 第7种写法
当 Kotlin Lambda 作为函数的最后一个参数时,Lambda 可以被挪到外面:
image.setOnClickListener() {
gotoPreview(it)
}
2-3-8 第8种写法
当 Kotlin 只有一个 Lambda 作为函数参数时,()
可以被省略:
image.setOnClickListener {
gotoPreview(it)
}
按照这个流程,在 IDE 里多写几遍,你自然就会理解了。一定要写,看文章是记不住的。
2-4 函数类型,高阶函数,Lambda表达式三者之间的关系
-
将函数的
参数类型
和返回值类型
抽象出来后,就得到了函数类型
。(View) -> Unit
就代表了参数类型
是 View返回值类型
为 Unit 的函数类型。 -
如果一个函数的参数
或者
返回值的类型是函数类型,那这个函数就是高阶函数
。很明显,我们刚刚就写了一个高阶函数,只是它比较简单而已。 -
Lambda 就是函数的一种
简写
一张图看懂:函数类型
,高阶函数
,Lambda表达式
三者之间的关系:
回过头再看官方文档提供的例子:
fun <T, R> Collection.fold(
initial: R,
combine: (acc: R, nextElement: T) -> R
): R {
var accumulator: R = initial
for (element: T in this) {
accumulator = combine(accumulator, element)
}
return accumulator
}
看看这个函数类型:(acc: R, nextElement: T) -> R
,是不是瞬间就懂了呢?这个函数接收两个参数,第一个参数类型是R
,第二个参数是T
,函数的返回类型是R
。
3. 带接收者(Receiver)的函数类型:A.(B,C) -> D
说实话,这个名字也对初学者不太友好:带接收者的函数类型
(Function Types With Receiver),这里面的每一个字(单词)我都认识,但单凭这么点信息,初学者真的很难理解它的本质。
还是绕不开一个问题:为什么?
3-1 为什么要引入:带接收者的函数类型?
我们在上一章节中提到过,用 apply 来简化逻辑,我们是这样写的:
修改前:
if (user != null) {
…
username.text = user.name
website.text = user.blog
image.setOnClickListener { gotoImagePreviewActivity(user) }
}
修改后:
user?.apply {
…
username.text = name
website.text = blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
请问:这个 apply 方法应该怎么实现?
上面的写法其实是简化后的 Lambda 表达式,让我们来反推,看看它简化前是什么样的:
// apply 肯定是个函数,所以有 (),只是被省略了
user?.apply() {
…
}
// Lambda 肯定是在 () 里面
user?.apply({ … })
// 由于 gotoImagePreviewActivity(this) 里的 this 代表了 user
// 所以 user 应该是 apply 函数的一个参数,而且参数名为:this
user?.apply({ this: User -> … })
所以,现在问题非常明确了,apply 其实接收一个 Lambda 表达式:{ this: User -> ... }
。让我们尝试来实现这个 apply 方法:
fun User.apply(block: (self: User) -> Unit): User{
block(self)
return this
}
user?.apply { self: User ->
…
username.text = self.name
website.text = self.blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
由于 Kotlin 里面的函数形参是不允许被命名为 this
的,因此我这里用的 self
,我们自己写出来的 apply 仍然还要通过 self.name
这样的方式来访问成员变量,但 Kotlin 的语言设计者能做到这样:
// 改为 this
// ↓
fun User.apply(block: (this: User) -> Unit): User{
// 这里还要传参数
// ↓
block(this)
return this
}
user?.apply { this: User ->
…
// this 可以省略
// ↓
username.text = this.name
website.text = blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
从上面的例子能看到,我们反推的 apply 实现比较繁琐,需要我们自己调用:block(this)
,因此 Kotlin 引入了带接收者的函数类型
,可以简化 apply 的定义:
// 带接收者的函数类型
// ↓
fun User.apply(block: User.() -> Unit): User{
// 不用再传this
// ↓
block()
return this
}
user?.apply { this: User ->
…
username.text = this.name
website.text = this.blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
现在,关键来了。上面的 apply 方法是不是看起来就像是在 User 里增加了一个成员方法 apply()?
class User() {
val name: String = “”
val blog: String = “”
fun apply() {
// 成员方法可以通过 this 访问成员变量
username.text = this.name
website.text = this.blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
}
所以,从外表上看,带接收者的函数类型,就等价于成员方法。但从本质上讲,它仍是通过编译器注入 this 来实现的。
一张图总结:
思考题2:
带接收者的函数类型,是否也能代表扩展函数?
思考题3:
请问:A.(B,C) -> D
代表了一个什么样的函数?
4. HTML Kotlin DSL 实战
官方文档在高阶函数的章节里提到了:用高阶函数来实现 类型安全的 HTML 构建器。官方文档的例子比较复杂,让我们来写一个简化版的练练手吧。
4-1 效果展示:
val htmlContent = html {
head {
title { “Kotlin Jetpack In Action” }
}
body {
h1 { “Kotlin Jetpack In Action”}
p { “-----------------------------------------” }
p { “A super-simple project demonstrating how to use Kotlin and Jetpack step by step.” }
p { “-----------------------------------------” }
p { “I made this project as simple as possible,” +
" so that we can focus on how to use Kotlin and Jetpack" +
" rather than understanding business logic." }
p {“We will rewrite it from “Java + MVC” to” +
" “Kotlin + Coroutines + Jetpack + Clean MVVM”," +
" line by line, commit by commit."}
p { “-----------------------------------------” }
p { “ScreenShot:” }
img(src = “https://user-gold-cdn.xitu.io/2020/6/15/172b55ce7bf25419?imageslim”,
alt = “Kotlin Jetpack In Action”)
}
}.toString()
println(htmlContent)
以上代码输出的内容是这样的:
Kotlin Jetpack In Action
Kotlin Jetpack In Action
A super-simple project demonstrating how to use Kotlin and Jetpack step by step.
I made this project as simple as possible, so that we can focus on how to use Kotlin and Jetpack rather than understanding business logic.
We will rewrite it from “Java + MVC” to “Kotlin + Coroutines + Jetpack + Clean MVVM”, line by line, commit by commit.
**自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/4d73cd50a764482b77d1721286b09a44.jpeg)
总结:
面试是一个不断学习、不断自我提升的过程,有机会还是出去面面,至少能想到查漏补缺效果,而且有些知识点,可能你自以为知道,但让你说,并不一定能说得很好。
有些东西有压力才有动力,而学到的知识点,都是钱(因为技术人员大部分情况是根据你的能力来定级、来发薪水的),技多不压身。
附上我的面试各大专题整理: 面试指南,满满的都是干货,希望对大家有帮助!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-PN3KPWwr-1712780637685)]
[外链图片转存中…(img-rTIMNetl-1712780637686)]
[外链图片转存中…(img-egYkNbxK-1712780637686)]
[外链图片转存中…(img-tH2TOdob-1712780637686)]
[外链图片转存中…(img-rTohe69N-1712780637686)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/4d73cd50a764482b77d1721286b09a44.jpeg)
总结:
面试是一个不断学习、不断自我提升的过程,有机会还是出去面面,至少能想到查漏补缺效果,而且有些知识点,可能你自以为知道,但让你说,并不一定能说得很好。
有些东西有压力才有动力,而学到的知识点,都是钱(因为技术人员大部分情况是根据你的能力来定级、来发薪水的),技多不压身。
附上我的面试各大专题整理: 面试指南,满满的都是干货,希望对大家有帮助!
[外链图片转存中…(img-o7reiir2-1712780637686)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!