一、Lambda简介
(1)函数代码块
函数式编码提供了一种解决问题的方法:把函数当作值来对待。
- 用匿名内部类是实现监听
button.setOnClickListener(new OnClickListener) {
@Override
public void onClick(View view) {
/* 点击后执行的动作 */
}
}
- 用lambda实现监听器
button.setOnClickListener { /* 点击后执行的动作 */ }
(2)集合
- 手动在集合中搜索
// 定义
fun findTheOldest(people: List<Person>) {
// 存储最大年龄
var maxAge = 0
// 存储年龄最大的人
var theOldest: Person? = null
for(person in people) {
// 如果下一个人比现在年龄最大的人还要大,改变最大值
if(person.age > maxAge) {
maxAge = person.age
theOldest = person
}
}
println(theOldest)
}
// 测试
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> findTheOldest(people)
Person(name=Bob, age=31)
- 用lambda在集合中搜索
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
// 比较年龄找到最大的元素
>>> println(people.maxBy { it.age })
Person(name="Bob", age=31)
maxBy函数可以在任何集合上调用,且只需要一个实参:一个函数,指定比较哪个值找到最大元素。
如果lambda刚好是函数或者属性的委托,可以用成员引用替换。
- 用成员引用搜索
people.maxBy(Person::age)
(3)Lambda表达式的语法
|---- 参数 ----| |-函数体-|
{ x: Int, y: Int -> x + y}
| --- 始终在花括号内 --- |
lambda表达式的语法
Kotlin的lambda表达式始终用花括号包围。注意实参并没有用括号起来。箭头把实参列表和lambda的函数体隔开。
lambda表达式可以存储在一个变量中,把额这个变量当作普通函数对待(即通过相应实参调用它)
>>> val sum = { x: Int, y: Int -> x + y }
// 调用保存在变量中的lambda
>>> println(sum(1, 2))
3
- 没有任何简明语法来重写这个例子
people.maxBy({ p: Person -> p.age })
Kotlin的语法约定:
-
- 如果lambda表达式是函数调用的最后一个实参,它可以放在括号的外边。
-
- 当lambda是函数唯一的实参时,还可以去掉调用代码中的空括号对。
-
- 若lambda是唯一的实参时,可以生路这些括号。
-
- 若lambda有多个实参时,既可以把lambda留着括号内来强调它是一个是实参,也可以把它放在括号的外面。
// 语法约定一
people.maxBy({ p: Person -> p.age})
// 语法约定二
people.maxBy() {p: Person -> p.age}
- 把lambda放在括号外传递
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> val names = People.joinToString(separator = " ",
transform = {p: Person -> p.name})
>>> println(names)
Alice Bob
- 把lambda放在括号外传递
people.joinToString(" ") { p: Person -> p.name }
// 显式地写出参数类型
people.maxBy { p: Person -> p.age }
// 推导出参数类型
people.maxBy { p -> p.age }
技巧:遵循这样一条简单的规则:先不声明类型,等编译器报错后再指定它们
- 使用默认参数名称
// “it”是自动生成的参数名称
people.maxBy { it.age }
仅在实参名称没有显式地指定时这个默认的名称才会生成。
it约定能大大缩短你的代码,但你不应该滥用它。尤其是在嵌套lambda的情况下,最好显式地声明每个lambda的参数,否则很难搞清楚it引用的到底是哪个值
(4)在作用域中访问变量
当在函数内声明一个匿名内部类时,能够在这个匿名类内部引这个函数的参数和局部变量。也可以用lambda做同样的事。如果在函数内部使用lambda,也可以访问这个函数的参数,还有在lambda之前定义的局部变量
- 在lambda中使用函数参数
// 定义
fun printMessageWithPrefix(messages: Collection<String>, prefix: String) {
messages.forEach {
println("$prefix $it")
}
}
// 测试
>>> val errors = listOf("403 Forbidden", "404 Not Found")
>>> printMessagesWithPrefix(errors, "Error:")
Error: 403 Forbidden
Error: 404 Not Found
-
区别: Kotlin和Java的一个显著区别就是,在Kotlin中不会限于访问final变量,在lambda内部也可以修改这些变量。
-
在lambda中改变局部变量
// 定义
fun printProblemCounts(responses: Collection<String>) {
var clientErrors = 0
var serverErrors = 0
responses.forEach {
if(it.startsWith("4")) clientErrors ++
else if(it.startsWith("5")) serverErrors ++
}
println("$clientErrors client errors, $serverErrors server errors")
}
// 测试
>>> val response = listOf("200 OK", "418 I'm a teapot", "500 Internal Server Error")
>>> printProblemCounts(responses)
1 client errors, 1 server errors
和Java不一样,Kotlin允许在lambda内部访问非final变量甚至修改它们。
注意:
默认情况下,局部变量的生命周期被限制在声明这个变量的函数中。但若它被lambda捕抓了,使用这个变量的代码可以被存储并稍后再执行。
捕抓final变量时,它的值和使用这个值的lambda代码一起存储。
对非final变量来说,它的值被封装在一个特殊的包装器中,这样你就可以改变这个值,而对这个包装器的引用会和lambda代码一起存储。
补充说明:为啥Java的匿名内部类只能访问final变量!
内部类对象的生命周期会超过局部变量的生命周期。局部变量的生命周期:当该方法被调用时,该方法中的局部变量在栈中被创建,当方法调用结束时,退栈,这些局部变量全部死亡。而内部类对象生命周期与其它类一样:自创建一个匿名内部类对象,系统为该对象分配内存,直到没有引用变量指向分配给该对象的内存,它才会死亡(被JVM垃圾回收)。所以完全可能出现的一种情况是:成员方法已调用结束,局部变量已死亡,但匿名内部类的对象仍然活着。这不被JAVA允许。
(内容来源于:JAVA匿名内部类不能访问外部类方法中的局部变量,除非变量被声明为final类型)
Java 只允许你捕抓final变量。当你想捕抓可变变量时,可以使用下面两种技巧:要么声明一个单元素的数组,其中存储可变值;要么创建一个包装类的实例,其中存储要改变的值的引用。
这里有一个重要的注意事项:
如果lambda被用作事件处理器或者用在其他异步执行的情况,对局部变量的修改只会在lambda执行时发生。
- 示例
fun tryToCountButtonClicks(button: Button): Int {
var clicks = 0
button.onClick { clicks ++}
return clicksd
}
这个方法始终返回0,。尽管onClick处理器可以修改clicks的值,你并不能观察到值的变化,因为onClick处理器是在函数返回之后调用的。
正确的做法是把点击次数存储在函数外依然可以访问的地方——例如类的属性,而不是存储在函数的局部变量中。
(5) 成员引用
::运算符:把函数转换成一个值
val getAge = Person::age
这种表达式称为成员引用,它提供了简明语法,来创建一个调用单个方法或者访问单个属性的函数值。 双冒号把类名称与你要引用的成员(一个方法或者一个属性)名称隔开。
| 类 | |成员|
Person :: age
|用冒号隔开|
这是一个更简洁的lambda表达式,它做同样的事情:
val getAge = { person: Person :: age}
注意,不管你引用的是函数还是属性,都不要在成员引用的名称后面加括号。
- 1.成员引用和调用该函数的lambda具有一样的类型,可以互换使用:
people.maxBy(Person::age) - 2.引用顶层函数(不是类的成员):
fun salate() = println("Salute!") run(::salute)。省略类名称的情况下,直接以::开头。成员引用::salute被当作实参传递给库函数run,它会调用相应的函数。 - 3.lambda委托给一个接受多个参数的函数,提供成员引用代替他将会非常方便。
// 这个lambda委托给sendEmail函数
val action = { person: Person, message: String ->
sendEmail(person, message)
// 可以用成员引用替代
val nextAction = ::sendEmail
- 4.用构造方法引用存储或者延期执行创建类实例的动作。构造方法引用的形式是在双冒号后制定类名称:
// 定义
data class Person(val name: String, val age: Int)
// 测试
>>> val createPerson = ::Person
>>> val p = createPerson("Alice", 29)
>>> println(p)
Person(name=Alice, age=29)
- 5.可以用同样的方法引用扩展函数:
fun Person.isAdult() = age >= 21
val predicate = Person::isAdult
尽管isAdult不是Person类的成员,还是可以通过引用访问它,这和访问实例的成员没什么两样:person.isAdult()