lambda 表达式应该都不陌生,在 Java8 中引入一个很重要的特性,将开发者从原来繁琐的语法中解放出来,但是局限于只有 Java8 版本才能使用。而 Kotlin 弥补了这一问题,Kotlin 中的 lambda 表达式与 Java 混合编程可以支持 Java8 以下的版本。
为什么使用 Kotlin 的 Lambda 表达式
针对 Kotlin 中使用 lambda 表达式的问题,主要有一下几点优点:
- Kotlin 的 lambda 表达式更加简洁易懂的语法实现功能,使开发者从原有冗余啰嗦的语法声明中解放出来。可以使用函数式编程中的过滤、映射、转换等操作符处理集合数据,从而使你的代码更加接近函数式编程的风格。
- Java8 以下的版本不支持 lambda 表达式,而 Kotlin 则兼容 Java8 以下版本有很好的互操作性,非常适合 Java8 以下版本与 Kotlin 混合开发的模式,解决 Java8 以下版本不能使用 lambda 表达是瓶颈。
- 在 Java8 版本使用 lambda 表达式是有限制的,它不是真正意义上的闭包,而 Kotlin 中的 lambda 才是真正意义上支持闭包的实现。(下面会阐述)
Kotlin 函数是一级函数,这意味着它们可以存储在变量和数据结构中,作为参数传递给其他高阶函数,也可以从其他高阶函数返回。对于其他非函数值,你可以以任何可能的方式对函数进行操作。
为了实现这一点,作为一种静态类型编程语言,Kotlin 使用一系列函数类型来表示函数,并提供了一组专门的语言构造,比如 lambda 表达式。
一、高阶函数
高阶函数是指接收函数作为参数(或返回函数)的函数。即一个函数可以将另一个函数当作参数(或返回函数),将其他函数用作参数(或返回函数)的函数被称为“高阶函数”。
(1)下面是一个高阶函数的示例:
fun stringMapper(str: String, mapper: (String) -> Int): Int {
// Invoke function
return mapper(str)
}
复制代码
stringMapper()
函数的参数是一个 String
以及一个函数,这个参数是将根据你传递给它的 String
来推导 Int
值。
要调用 stringMapper()
,可以传递一个 String
和一个满足第二个参数条件的函数(即一个将 String
当作输入并输出 Int
的函数),如下示例:
stringMapper("Android", { input ->
input.length
})
复制代码
如果匿名函数是在某个函数上定义的最后一个参数,则你可以在用于调用该函数的圆括号 ()
之外传递这个参数,如下所示:
stringMapper("Android") { input ->
input.length
}
复制代码
(2)另一个很好的例子就是针对集合的 函数式编程习语折叠,它接受一个初始的累加器值和一个组合函数,并通过连续地将当前的累加器值与每个集合元素组合来构建它的返回值,替换这个叠加器:
//从[初始]值开始累加值,从左到右对当前累加器值和每个元素应用[操作]
fun Collection.fold(
initial: R,
combine: (acc: R, nextElement: T) -> R
): R {
var accumulator = initial
for (element: T in this) {
accumulator = combine(accumulator, element)
}
return accumulator
}
复制代码
在上面的代码中,参数 combine
有一个函数类型 (R, T) -> R
,因此它接收一个函数,该函数接收两个参数类型 R
和 T
,并返回一个类型 R
的值。它在 for 循环中调用,然后将返回值分配给 accumulator
。
调用 fold
,我们需要给它传递一个函数类型的实例作为参数,lambda表达式(在下面描述的更详细)在高阶函数调用站点被广泛用于这种情况:
val items = arrayOf(1, 2, 3, 4, 5)
//Lambdas是大括号括起来的代码块
items.fold(0, { acc: Int, i: Int -> //当lambda 有参数时,参数在前面,然后是 `->` 符号
print("acc == $acc | i == $i | ")
val result = acc + i
println("result == $result")
//如果不显式指定,lambda 中的最后一个表达式被认定为是返回值
result
})
//参数类型在lambda中如果能自动推断则可以省略
val itemsStr = items.fold("Elements:", { acc, i -> acc + "" + i })
//函数引用也可以用于高阶函数调用
val products = items.fold(1, Int::times)
复制代码
打印数据如下:
acc == 0 | i == 1 | result == 1
acc == 1 | i == 2 | result == 3
acc == 3 | i == 3 | result == 6
acc == 6 | i == 4 | result == 10
acc == 10 | i == 5 | result == 15
复制代码
1.1 函数类型
Kotlin 使用一系列函数类型,比如 (Int) -> String
来声明函数:val onClick: () -> Unit = ...
。这些类型有一个特殊的符号,对应函数的签名,即它们的参数和返回值:
(A, B) -> C
: 所有的函数类型有一个带括号的参数类型列表和一个返回类型。(A, B) -> C
表示一个函数类型,该类型表示具有类型 A 和 B 的两个参数并返回类型 C 的值的函数。参数类型列表可以是空的,如() -> A
。不能省略Unit
返回类型;A.(B) -> C
: 函数类型可以有一个附加的接收类型,它在点符号.
之前指定。类型A.(B) -> C
表示可以在接收对象 A 调用 B 类型的参数,返回值为 C 类型的函数。带有接收器的函数文字通常与这些类型一起使用;- 挂起函数 : 挂起函数属性特性类型的函数,在表示法中有一个挂起修饰符。例如:
suspend () -> Unit
或者suspend A.(B) -> C
。
函数类型表示法可以选择包括函数参数的名称:(x: Int, y: Int) -> Point
。这些名称可用于记录参数的含义。
1.要指定一个函数类型可以为空,可以使用括号:
((Int, Int) -> Int)?
2.函数类型可以使用括号组合:
(Int) -> ((Int) -> Unit)
3.箭头符号
->
是右结合的,(Int) -> (Int) -> Unit
与(Int) -> ((Int) -> Unit)
表示同一个类型,但是与函数类型((Int) -> (Int)) -> Unit
不同。
你也可以使用类型别名给一个函数类型一个代替名称:
typealias ClickHandler = (Button, ClickEvent) -> Unit
复制代码
1.2 实例化函数类型
下面有几种方法可以获得一个函数类型的实例:
(1)在函数文字中使用代码块,形式如下:
- 一个 lambda 表达式:
{ a, b -> a + b }
; - 一个匿名函数:
fun(s: String): Int { return s.toIntOrNull() ?: 0 }
。
带有 receiver 的函数文本可以用作带有 receiver 的函数类型的值。
(2)使用实现函数类型作为接口的自定义类的实例:
//定义IntTransformer类型,实现了 (Int) -> Int 接口
class IntTransformer : (Int) -> Int {
override operator fun invoke(num: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()
复制代码
如果数据足够明朗,编译器可以推断出变量的函数类型:
val result = { i: Int -> i * 2 }//推断出的类型是(Int) -> Int
复制代码
对于包含 receiver 的方法类型,如果 receiver 的类型以及该方法类型的剩余参数类型和没有显式定义 receiver 的方法类型的入参相匹配,则二者可以进行相护赋值。比如方法类型 String.(Int) -> Boolean
就是显式包含 receiver 的方法类型,该 receiver 的方法类型是 String
,参数类型是 Int
,所以等同于方法类型 (String, Int) -> Boolean
,这个方法类型接收一个 String
类型和一个 Int
类型参数,而第一个 String
类型刚好和 receiver 相匹配,剩下的参数类型也相互匹配,可以认为它们是相等的。
//String,Int传入的参数类型后面的String表示返回值类型,times表示Int类型参数
val substringStr: String.(Int) -> String = { times ->
this.substring(0, times)//times为5
}
val twoParameters: (String, Int) -> String = substringStr
//(A, B) -> C 所有的函数类型有一个带括号的参数类型列表和一个返回类型,
//表示一个类型,该类型表示具有类型 A 和 B 的两个参数并返回类型 C 的值的函数。
fun runTransformation(ss: (String, Int) -> String): String {
return ss("Android", 5)
}
val trans = runTransformation(substringStr) //substringStr()函数作为参数传递给runTransformation()函数
复制代码
打印数据如下:
Andro
复制代码
注意:没有接收方的函数类型在默认情况下会被自动推断出来,即使使用扩展函数的引用初始化变量也是如此。如果要更改,请显式指定变量类型。
1.3 调用函数类型实例
函数类型的值可以通过使用其 invoke(...)
操作符来调用:f.invoke(x)
或者 f(x)
。invoke()
表示通过 函数变量
调用自身。
如果值具有 receiver 类型,则 receiver 对象应作为第一个参数传递。用 receiver 调用函数类型的值的另一种方法是在它前面加上 receiver 对象,就好像这个值是一个扩展函数:1.foo(2)
。
//String.plus()通过将该字符串与给定的其他对象的字符串表示形式链接起来而获得的字符串
val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus //Int.plus()将另一个值添加到此值
val str = stringPlus.invoke("") //打印:
val str2 = stringPlus("Hello", "World") //打印:HelloWorld
val int1 = intPlus(1, 1) //打印:2
val int2 = intPlus.invoke(2, 3) //打印:5
val int3 = 4.intPlus(5) //打印:9
复制代码
二、Lambda表达式
Lambda 表达式本质其实是匿名函数,因为底层的实现还是匿名函数来实现的,但是我们在使用的时候其实不太需要关心底层是怎么实现的,Lambda 的出现确实减少了代码量的编写,变得更加简洁。
Lambda 表达式和匿名函数都是“字面函数”,也就是说,在没有显式定义方法的时候,可以通过这种方式生成一个具有同等形式,功能的方法。
strMax(strings, { a, b -> a.length > b.length })
复制代码
函数 strMax()
是一个高阶函数,它采用函数值作为第二个参数。第二个参数是一个表达式 { a, b -> a.length > b.length }
,它本身是一个函数,即一个函数字面量,它等价于下面的命名函数:
fun compare(a: String, b: String): Boolean = a.length < b.length
复制代码
2.1 Lambda表达式分类
在 Kotlin 中实际可以将 lambda 表达式分为两大类,一个是普通的 lambda 表达式,另一个是带接收者的 lambda 表达式,两种表达式在使用和使用场景也是有很大的不同。先看下两种 lambda 表达式的类型声明: 针对带接收者的 lambda 表达式在 Kotlin 标准库中也是非常常见的,比如 with,apply 等标准函数的声明:
@kotlin.internal.InlineOnly
public inline fun with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
@kotlin.internal.InlineOnly
public inline fun T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
复制代码
看到上面 lambda 表达式的分类,你有没有想到之前的扩展函数: 是不是和我们之前的普通函数和扩展函数类似?普通的 lambda 表达式类似对应普通的函数声明,而带接收者函数的 lambda 表达式类似对应扩展函数。扩展函数就是这种声明接收者类型,然后使用接收者对象调用类似成员函数调用,实际内部就是通过这个接收者对象实例直接访问它的属性和方法。
2.2 Lambda表达式语法
lambda 表达式的标准形式基本声明满足三个条件:
- 1、含实际参数;
- 2、含函数体(如果函数体为空也要声明出来);
- 3、以上内容必须被包含在大括号内。
以上是 lambda 表达式最标准的形式,可能这种标准形式以后在日常开发中很少看见,更多是更加简化的形式。(下面会介绍 lambda 表达式简化的规则)
lambda表达式完整句语法形式如下:
//显式定义sum的类型为(Int, Int) -> Int的函数类型
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
复制代码
lambda 表达式总是在大括号 {}
包裹着,完整语法形式的参数声明在大括号内,并有可以省略的参数类型,body 在 ->
符号之后。如果 lambda 的推断返回类型不是 Unit
,则 lambda 主体内的最后一个(或者可能是单个)表达式将被默认视为返回值。
//也可以给sum直接赋值一个函数类型实例,等于后面的lambda表达式
val sum = { x: Int, y: Int -> x + y }
复制代码
如果我们去掉所有可选的参数类型: (Int, Int) -> Int
,编译器能自动推断,简化后就是上面的样式。
表达式类型
() -> Unit //表示无参数无返回值的Lambda表达式类型
(T) -> Unit //表示接收一个T类型参数,无返回值的Lambda表达式类型
(T) -> R //表示接收一个T类型参数,返回一个R类型值的Lambda表达式类型
(T, P) -> R //表示接收一个T类型和P类型的参数,返回一个R类型值的Lambda表达式类型
(T, (P, Q) -> S) -> R //表示接收一个T类型参数和一个接收P、Q类型两个参数并返回一个S类型的值的Lambda表达式类型参数,返回一个R类型值的Lambda表达式类型
复制代码
前面的几种情况比较好理解,最后一种有点难度,最后一种实际上属于高阶函数的范畴,这种类型有点像剥洋葱那样层层往内拆分,就是由外往内看,层层拆分,对于本身是一个 lambda 表达式类型的,先暂时看做一个整体,然后就可以确定最外层的 lambda 类型,如此类推往内部拆分。 (1)无参数语法
语法格式:
val/var = { …… }
复制代码
案例:
//源码
fun noParameter() {
println("无参数lambda")
}
//lambda表达式
val noParameter = { println("无参数lambda") }
//调用
fun main(args: Array) {
noParameter()
}
复制代码
lambda表达式总是被大括号包括着。
(2)有参数语法
语法格式:
val/var : (,,...) -> 返回值类型 = { 参数,参数,... -> 操作参数的代码 }
//等价于:
val/var = { 参数:,参数:,... -> 操作参数的代码 }
复制代码
案例:
//源码
fun hasParameter(a: Int, b: Int): Int {
return a + b
}
//lambda表达式
val hasParameter: (Int, Int) -> Int = { a, b -> a + b }
//简化后
val hasParameter2 = { a: Int, b: Int -> a + b }
//调用
fun main(args: Array) {
hasParameter(4, 2)//打印 6
}
复制代码
完整的 lambda 表达式如上述所示,它有完整的参数类型和表达式返回值,可以把一些类型标注省略的时候(如 hasParameter2 ),当推断出来的返回值类型不为 Unit
时,它的返回值即为 ->
后面代码的最后一个表达式的类型。(lambda返回值下面会讲到)
(3)lambda表达式作为函数中的参数时
语法格式:
fun test(arg : Int, : (参数 : 类型, 参数 : 类型, ... ) -> 表达式返回类型) {
//TODO
}
复制代码
案例:
//源码
fun expressionParameter(numA: Int, numB: Int): Int {
}
//lambda表达式,numB只提供了参数类型和返回类型,调用时需要写出它
fun expressionParameter(numA: Int, numB: (a: Int, b: Int) -> Int): Int {
return numA * numB.invoke(3, 4) //a * ( 3 + 4 ),`invoke()` 表示通过 `函数变量` 调用自身。
}
//lambda表达式,这里作为上面函数的参数使用
val expression: (Int, Int) -> Int = { a, b -> a + b }
//调用
fun main(args: Array) {
//第二个参数需要写出它的具体实现
expressionParameter(2, expression)//打印为 14
}
复制代码
上述情况表示为高阶函数,当 lambda 表达式作为一个参数时,只为其表达式提供了参数类型和返回类型,所以在调用高阶函数的时候要写出该表达式的具体实现。
expressionParameter(numA: Int, numB: (a: Int, b: Int) -> Int)
中参数 numB 只提供了参数类型和返回类型,调用时需要写出参数 numB 的具体实现 { a, b -> a + b }
。
2.3 lambdas 语法简化转换
lambda表达式可以大大简化代码写法,也能减少不必要的方法定义,但是带来的副作用是代码的可读性大大降低。下面来介绍一下lambda表达式简化的几种方法,以下面的例子为例:
//check表示传入一个Int类型参数,返回一个Boolean类型返回值
fun getResult(a: Int, check: (Int) -> Boolean): String {
val result = if (check(a)) "Android" else "null"
return result
}
//调用上面的函数
getResult(10, { num: Int -> num > 0 })
复制代码
第二个参数 { num: Int -> num > 0 }
符合 check: (Int) -> Boolean
函数类型,表达式用 {}
包裹,num 为该表达式传入的参数, ->
符号之后是需要操作的代码。
(1)如果一个函数的最后一个参数是一个函数,那么作为对应参数传递的 lambda 表达式可以放在圆括号 ()
外:
//getResult()函数的最后一个参数{ num: Int -> num > 0 }被提到函数括号外面
getResult(10) { num: Int -> num > 0 }
复制代码
这种语法也称为尾随 lambda。
(2)如果 lambda 是被调用的函数的唯一一个参数,函数的圆括号 ()
可以被完全省略:
//只有一个参数
fun getResult(check: (Int) -> Boolean) {
}
//简化前
getResult() { num: Int -> num > 0 }
//简化后,{ num: Int -> num > 0 }是setText函数的唯一一个参数,被提到圆括号外面并且圆括号可以被省略
getResult { num: Int -> num > 0 }
复制代码
(3) it
单个参数的隐式名称
在高阶函数中,如果 lambda 表达式的参数只有一个,那么不允许声明唯一的参数并且省略了 ->
符号,用 it
表示这个参数的名称。
//简化前
getResult(10) { num: Int -> num > 0 }
//简化后,it表示参数名称,即num
getResult(10) {
it > 0
}
复制代码
it
不是关键字,表示单个参数的隐式名称。
以后开发中我们更多的是使用简化版的 lambda 表达式,因为标准版的 lambda 表达式还是有点啰嗦,比如实参类型就可以省略,Kotlin 支持根据上下文智能推导出类型,所以可以省略,摒弃啰嗦的语法,下面总结了一下: 注意:语法简化是把双刃剑,使用简单方便,但是不能滥用,也需要考虑代码的可读性。上图中的 lambda 化简成最简形式用 it
这种,一般在多个 lambda 嵌套时不建议使用,大大降低代码可读性,最后连开发者都不知道 it
代表什么。
(4)例子
这个是 Kotlin 库中的 joinToString 扩展函数,最后一个参数是一个 接收一个集合类型T的参数返回一个CharSequence类型的
lambda 表达式。
//joinToString内部声明
public fun Array.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}
//调用
fun main(args: Array) {
val items = arrayOf(1, 2, 3, 4, 5)
items.joinToString(separator = ",", prefix = "") {
return@joinToString "index$it"
}
}
复制代码
可以看到 joinToString
调用的地方使用了 lambda 表达式作为参数的简化形式,将它从括号 ()
中提出来了。这个确实给调用带来了一点小疑惑,因为并没有显式表明 lambda 表达式应用到哪里,所以不熟悉内部实现的开发者很难理解,对于这种问题,Kotlin 实际上提供了解决方法,也就是命名参数:
//调用
fun main(args: Array) {
val items = arrayOf(1, 2, 3, 4, 5)
items.joinToString(separator = ",", prefix = "", transform = { "index$it" })
}
复制代码
2.4 从lambda表达式返回值
如果 lambda 表达式有返回值,则 lambda 主体内的最后一行表达式将被视为返回值返回,即lambda会将最后一条语句作为其返回值。如果使用限定的返回语法显式地从 lambda 返回一个值,则不会再以默认的最后一行返回值。
下面两种表示方式是等价的:
//check表示传入一个Int类型参数,返回一个Boolean类型返回值
fun getResult(a: Int, check: (Int) -> Boolean): String {
val result = if (check(a)) "Android" else "null"
return result
}
//调用
getResult(10) {//it 表示参数名称
val result = it > 0
result //lambda 表达式默认最后一行为返回值
}
val apple = getResult(10) {
val result = it > 0
return@getResult result //限定的返回语法指定返回值,这里直接返回了true
false //上面使用了限定的返回语法指定返回值,不再以默认最后一行为返回值
}
println("getResult == $apple") //打印 getResult == Android
复制代码
这个习惯,连在括号外传递一个 lambda 表达式,允许链式风格的代码:
val strs = arrayOf("sum", "java", "android", "kotlin")
strs.filter { it.length == 5 }.sortedBy { it }.map { it.toUpperCase() }
复制代码
2.5 下划线用于未使用的变量(自1.1以来)
如果 lambda 参数未使用,你可以用下划线 _
来代替参数的名称:
//如果参数未使用,用下划线 `_` 来代替它的名称
val radioGroup = RadioGroup(this)
//原型
radioGroup.setOnCheckedChangeListener { group, checkedId ->
}
//简化后,参数group未被使用,用下划线 `_` 来代替
if (checkedId == 0) {
//TODO
}
}
复制代码
下划线 _
表示未使用的参数,不处理这个参数。
2.6 使用typealias关键字给Lambda类型命名
我们试想一个场景就是可能会用到多个 Lambda表达式,但是这些 Lambda表达式的类型很多相同,我们就很容易把一大堆的Lambda类型重复声明或者Lambda类型声明太长不利于阅读。实际上不需要,Kotlin 反对一切啰嗦语法,它都会给你提供一系列的解决办法,让你简化代码的同时又不降低代码的可读性。
fun main(args: Array) {
val strEmpty: (String) -> Unit = {
if (it.isEmpty()) {
//TODO
}
}
val strTrue: (String) -> Unit = {
if (it.equals("true")) {
//TODO
}
}
}
复制代码
使用 typealias 关键字声明 (String) -> Unit
类型:
typealias StringEmpty = (String) -> Unit //在最顶层
fun main(args: Array) {
val strEmpty: StringEmpty = {
if (it.isEmpty()) {
//TODO
}
}
val strTrue: StringEmpty = {
if (it.equals("true")) {
//TODO
}
}
}
复制代码
三、Lambda表达式常用场景
3.1 场景一:
lambda 表达式与集合一起使用是最常见的场景,可以各种帅选、映射、变换操作符和对集合数据进行各种操作,非常灵活,类似使用 RxJava 函数式编程,Kotlin 在语言层面无需增加额外库,就给你提供函数式编程API。
val lists = arrayListOf("Java", "Android", "Kotlin")
lists.filter {
it.startsWith("K")
}.map {
"$it 是一门非常好的语言!"
}.forEach {
println(it)//Kotlin 是一门非常好的语言!
}
复制代码
3.2 场景二:
替代原有匿名内部类,但是注意只能替代单抽象方法的类。
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//TODO
}
});
复制代码
用 Kotlin lambda 实现,等价于:
textView.setOnClickListener {
//TODO
}
复制代码
3.3 场景三:
定义 Kotlin 扩展函数或者说需要把某个操作或者函数当作值传入某个函数的时候。
fun showDialog(content: String = "弹框", negativeText: String = "取消", positiveText: String = "确定", negativeAction: (() -> Unit)? = null, positiveAction: (() -> Unit)? = null) {
AlertDialog.Builder(this)
.setMessage(content)
.setNegativeButton(negativeText) { _, _ ->
negativeAction?.invoke()
}
.setPositiveButton(positiveText) { _, _ ->
positiveAction?.invoke()
}
.setCancelable(true)
.create()
.show()
}
复制代码
四、Lambda表达式的作用域中访问变量
4.1 Kotlin和Java访问局部变量的区别
(1)在 Java 函数内部定义一个匿名内部类或者 lambda,内部类访问的局部变量必须是 final
修饰的,意味着在内部类内部或者 lambda 表达式的内部无法修改函数局部变量的值。
//Java
public class ExampleActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_example);
final int count = 0;//需要使用final修饰
findViewById(R.id.btn_syn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println(count);//在匿名OnClickListener内部类访问count必须要是final修饰的
}
});
}
复制代码
(2)Kotlin 中在函数定义的内部类或 lambda,既可以访问 final
修饰的变量,也可以访问非 final
修饰的变量,意味着 lambda 内部是可以直接修改函数局部变量的值。
class LambdasActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val textView = TextView(this)
var count = 0//声明非final类型
val countFinal = 0//声明final类型
textView.setOnClickListener {
println(count++)//访问并修改非final修饰的变量
println(countFinal)//访问final修饰的变量,和Java一样
}
}
}
复制代码
通过以上对比发现,Kotlin 使用 lambda 比 Java 中使用 lambda 更灵活,访问限制更少。Kotlin 中的 lambda 表达式是真正意义上支持闭包,而 Java 中的 lambda 则不是。下面来分析 Kotlin 是怎么做到这一点的?
4.2 Kotlin中Lambda表达式的变量捕获以及原理
什么是变量捕获?
通过上面的例子,我们知道在 Kotlin 中既能访问 final
修饰的变量也能访问和修改非 final
修饰的变量。这里涉及到一个概念lambda表达式的变量捕获,实际上就是 lambda 表达式在函数体内可以访问和修改外部变量,我们就称为这些变量被 lambda 表达式捕获了。
有了这个概念我们把上面的总结一下:
- 1、在 Java 中 lambda 表达式只能捕获
final
修饰的变量; - 2、在 Kotlin 中 lambda 表达式既能捕获
final
修饰的变量也能访问和修改非final
修饰的变量。
变量捕获的原理
我们知道函数局部生命周期属性这个函数,当函数执行完毕,局部变量也被销毁了,但是如果这局部变量被 lambda 捕获了,那么使用这个局部变量的代码将被存储起来等待稍后再次执行,也就是被捕获的变量可以延迟生命周期的。
- 1、lambda 捕获
final
修饰的局部变量原理: 局部变量的值和使用这个变量的表达式被存储起来; - 2、lambda 捕获非
final
修饰的局部变量原理: 实际是 lambda 表达式还是只能捕获final
修饰的变量,而为什么 Kotlin 能做到修改非final
变量的值,Kotlin 在语层面做了桥接包装,它把非final
修饰的变量使用一个 Ref 包装类包装起来,然后外部保留 Ref 包装类的引用是final
的,然后表达式和这个final
修饰的 Ref 引用一起存储,随后在 lambda 内部修改变量的值实际上是通过这个final
的 Ref 包装类引用去修改的。
最后通过查看 Kotlin 修改非 final
局部变量的反编译成的 Java 代码:
class LambdasActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val textView = TextView(this)
var count = 0//声明非final类型
textView.setOnClickListener {
println(count++)//访问并修改非final修饰的变量
}
}
}
复制代码
反编译后:
操作:Android studio > Tools > Kotlin > Show Kotlin Bytecode > 点击 Decompile > Kotlin类即可反编译成 Java类
@Metadata(
mv = {1, 1, 18},
bv = {1, 0, 3},
k = 1,
d1 = {"u0000u0018nu0002u0018u0002nu0002u0018u0002nu0002bu0002nu0002u0010u0002nu0000nu0002u0018u0002nu0000u0018u00002u00020u0001Bu0005¢u0006u0002u0010u0002Ju0012u0010u0003u001au00020u00042bu0010u0005u001au0004u0018u00010u0006Hu0014¨u0006u0007"},
d2 = {"Lcom/suming/kotlindemo/blog/LambdasActivity;", "Landroidx/appcompat/app/AppCompatActivity;", "()V", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "app"}
)
public final class LambdasActivity extends AppCompatActivity {
private HashMap _$_findViewCache;
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView textView = new TextView((Context)this);
final IntRef count = new IntRef();//IntRef特殊的包装器类型,final修饰IntRef的引用count
count.element = 0;//包装器内部的非final变量
textView.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void onClick(View it) {
IntRef var10000 = count;
int var2;
var10000.element = (var2 = var10000.element) + 1;
boolean var3 = false;
System.out.println(var2);
}
}));
}
public View _$_findCachedViewById(int var1) {
if (this._$_findViewCache == null) {
this._$_findViewCache = new HashMap();
}
View var2 = (View)this._$_findViewCache.get(var1);
if (var2 == null) {
var2 = this.findViewById(var1);
this._$_findViewCache.put(var1, var2);
}
return var2;
}
public void _$_clearFindViewByIdCache() {
if (this._$_findViewCache != null) {
this._$_findViewCache.clear();
}
}
}
复制代码
注意:对于在 lambda 表达式内部修改局部变量的值,只会在这个 lambda 表达式被执行的时候触发。
五、Lambda 表达式的成员引用
5.1 为什么使用成员引用?
Lambda 表达式可以直接把一个代码块作为一个参数传递给函数,但是如果要当做参数传递的代码已经被定义成了函数,此时还需要重复写一个代码块传递过去吗?肯定不是,这时候就需要 把函数转换成一个值,这种方式称为 成员引用。
fun main(args: Array) {
val persons = arrayListOf(Person("Java", 20), Person("Android", 5))
println(persons.maxBy({ p: Person -> p.age }))
}
复制代码
{ p: Person -> p.age }
表达式可以用成员引用 Person::age
替换。成员引用和调用函数的 lambda 具有一样的类型,所以可以相互转换。如下:
fun main(args: Array) {
val persons = arrayListOf(Person("Java", 20), Person("Android", 5))
//println(persons.maxBy({ p: Person -> p.age }))
println(persons.maxBy(Person::age))//成员的引用类型和maxBy()传入Lambda表达式的一致
}
复制代码
5.2 成员引用的基本语法
上面这种用法称为成员引用,它提供简明语法,来创建一个单个方法或者单个属性的函数值,使用 ::
运算符来转换。成员引用由类名,双冒号,成员三个元素组成。成员是函数名表示引用函数,成员是属性表示引用属性。
5.3 成员引用的使用场景
(1)最常见的使用方式是类名+双冒号+成员(属性或函数):
fun main(args: Array) {
val persons = arrayListOf(Person("Java", 20), Person("Android", 5))
println(persons.maxBy(Person::age))//成员的引用类型和maxBy()传入Lambda表达式的一致
}
复制代码
(2)还可以引用顶层函数,这种情况省略了类名称,直接以 ::
开头。成员引用 ::salute
被当作实参传递给库函数 run()
,它会调用相应的函数:
fun saulte() = println("fun saulte") //顶层函数
fun main(args: Array) {
//使用函数引用前
run({ saulte() })
//使用函数引用简化后
run(::saulte)//打印:fun saulte
}
复制代码
(3)如果 lambda 要委托给一个接收多个参数的函数,提供成员引用代替它将会非常方便:
//有多个参数的 lambda
val action = { person: Person, message: String -> sendEmail(person, message) }
//使用成员引用代替
val nextAction = ::sendEmail
//调用
nextAction(Person(), "msg")
复制代码
(4)成员引用用于构造方法。可以用构造方法的引用存储或者延期执行创建类实例的动作,构造方法的引用方式是在双冒号后指定类名称: ::类名
fun main(args: Array) {
val getPerson = ::Person //创建的实例动作就保存成了值
println("构造方法 == " + getPerson("Kotlin", 3))//打印 Person(name=Kotlin, age=3)
}
复制代码
(5)成员引用还可以使用同样的方式引用扩展函数:
// 这是Person的一个扩展函数,判断是否成年
fun Person.isChild() = age > 18
fun main(args: Array) {
val isChild = Person::isChild
println("isChild == " + isChild(Person("Java", 20)))//打印 true
}
复制代码
5.4 绑定引用
Kotlin1.1 以上允许你使用成员引用语法 捕捉特定实例对象上的方法引用。
val person = Person("Android", 24) //创建实例
//val personAge = { person.age } //Kotlin1.1之前显式写出 lambda
val personAge = person::age //Kotlin1.1之后可以使用绑定引用
println("person: age == ${personAge()}")//打印 person: age == 24
复制代码
注意:personAge 是一个零函数的参数,在Kotlin1.1之前你需要显式写出 lambda { person.age }
,而不是使用绑定成员引用:person::age
。
六、匿名函数
6.1 匿名函数
并非每个函数都需要一个名称,某些函数通过输入和输出更直接地进行标识,这些函数称为匿名函数。匿名函数都是函数字面量,都可以在不显式定义方法的时候提供提供具有同样形式,功能的实现。匿名函数可以明确指定返回类型,匿名函数与常规函数不同的是没有函数名。
你可以保留某个匿名函数的引用,方便以后使用此引用来调用该匿名函数,与其他引用类型一样,你也可以在引用中间传递引用。
val stringLengthFunc: (String) -> Int = { input ->
input.length
}
复制代码
与常规命名函数一样,匿名函数也可以包含任意数量的表达式,函数返回值是最终表达式的结果。
在上面实例中,stringLengthFunc
包含一个匿名函数的引用,该函数将 String
当作输入,并将输入 String
的长度作为 Int
类型的输出返回。因此,该函数的类型表示为 (String) -> Int
。不过,此代码不会调用该函数,如果要检索该函数的结果,你必须像调用命名函数一样调用该函数。调用 stringLengthFunc
时,必须提供 String
,如下所示:
val stringLengthFunc: (String) -> Int = { input ->
input.length
}
val stringLength: Int = stringLengthFunc("Android")
复制代码
lambda 表达式语法中缺少的一点是指定函数返回类型的能力。在大多数情况下,这是不必要的,因为可以自动推断返回类型。但是,如果你确定需要显式地指定它,可以使用匿名函数。
//常规函数
fun test(x: Int, y: Int): Int = x + y
//匿名函数
fun(x: Int, y: Int): Int = x + y
复制代码
匿名函数看起来很像常规函数声明,除了它的名字被省略。它的主体可以是一个表达式(如上述)或一个块:
fun(x: Int, y: Int): Int {
return x + y
}
复制代码
参数和返回类型的指定方式与常规函数相同,除了参数类型可以从上下文推断而省略:
val items = arrayOf(1, 2, 3, 4, 5)
//返回符合条件的元素的列表
val list = items.filter(fun(item) = item > 2)
println("list == $list")//打印 [3, 4, 5]
复制代码
匿名函数的返回类型推断与普通函数的工作方式一样:对于带有表达式体的匿名函数,返回类型自动推断,对于带有块体的匿名函数,返回类型必须显式指定(或者假设为Unit
)。
注意:匿名函数参数总是在括号内传递。允许将函数放在括号外的简写语法仅适用于lambda表达式。
lambda 表达式和匿名函数之间的另一个区别是非本地返回的行为。没有标签的 return
语句总是从使用 fun
关键字声明的函数返回。这意味着 lambda 表达式中的 return
将从封闭函数返回,而匿名函数内部的 return
将从匿名函数本身返回。
6.2 带有接收器的函数字面量
在 Kotlin 中,提供了指定接收者对象调用 Lambda 表达式的功能。在函数字面值的函数体中,可以调用该接收者对象上的方法而无需额外的限定符。它类似于扩展函数,允许你在函数体内访问接收者对象的成员。
带有接收器的函数类型,例如:A.(B) -> C
,可以用函数字面量的特殊形式实例化——带有接收器的函数字面量。
在字面量函数体中,传递给调用的 receiver 对象变成隐式的 this
,这样你就可以访问该 receiver 对象的成员而不需要任何附加限定符,或者使用 this
表达式访问 receiver 对象。
这里有一个例子:一个带有接收器及其类型的函数字面量,其中 plus
在接收器对象被调用。
val all: Int.(Int) -> Int = { other -> plus(other) }
复制代码
匿名函数语法允许你直接指定函数字面量的接收方类型。如果你需要使用 receiver 声明一个函数类型的变量,并在以后使用它,那么这将非常有用。
//直接指定函数字面量的接收方类型
val all = fun Int.(other: Int): Int = this + other
复制代码
当可以从上下文推断接收者类型时,Lambda表达式可以作为函数字面量与接收者一起使用。它们使用的一个最重要的例子是类型安全构建器 type-safe builders
。
class Student {
fun body() {}
}
fun study(init: Student.() -> Unit): Student {
val student = Student()//创建接收器对象
student.init()//将接收器对象传递给lambda
return student
}
//调用
study {//带有接收器的lambda从这里开始
body()//调用接收器对象上的方法
}
复制代码
七、总结
(A, B) -> C
:有一个带括号的参数类型列表和一个返回类型; A.(B) -> C
:可以在接收对象 A 调用 B 类型的参数,返回值为 C 类型的函数; suspend A.(B) -> C
:挂起函数属性特性类型的函数,在表示法中有一个挂起修饰符。invoke(...)
操作符来调用,invoke()
表示通过 函数变量
调用自身。val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
。 lambda 表达式总是在大括号 {}
包裹着,完整语法形式的参数声明在大括号内,并有可以省略的参数类型,body 在 ->
符号之后。A.如果函数 A 的最后一个参数是函数 B,那么作为对应参数传递的 lambda 表达式可以放在函数 A 圆括号 () 外;
B.如果 lambda 表达式是被调用的函数的唯一一个参数,则函数的圆括号 () 可以被完全省略; C.如果 lambda 表达式的参数只有一个,那么不允许声明唯一的参数并且省略了 -> 符号,用 it
表示这个参数的名称。
_
来代替参数的名称。return
将从封闭函数返回,而匿名函数内部的 return
将从匿名函数本身返回。