第五章 Lambda编程
lambda本质就是可以传递给其他函数的一小段代码。
5.1 Lambda表达式和成员引用
5.1.1 Lambda简介:作为函数参数的代码块
在代码中存储和传递一小段行为是常有的任务,为了简洁的达成目的,函数式编程提出可以把函数当做值来对待,使用lambda表达式后会更加简洁。
代码对比示例:
//java
button.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View view){
//点击后执行的动作
}
})
//kotlin用lambda
button.setOnClickListener{/*点击后执行的动作/*}
5.1.2 Lambda和集合
对集合执行的大部分任务遵循几个通用模式,是个很好的展现lambda的场景。
下面的只是代码示例,看一哈即可。详细解读在后面章节。
//比较年龄找到最大的元素
//省略常规for循环,直接看lambda代码感受
data class Person(val name:String,val age:Int)
val people = listOf(Person("A",29),Person("B",33))
println(people.maxBy{it.age})
>>>>> People(name = A,age = B)
特别说明下maxBy,现在1.4版本已经升级为maxByOrNull。
下面为maxByOrNull的源码实现,上面的lambda表达式就是参数。
public inline fun <T, R : Comparable<R>> Iterable<T>.maxByOrNull(selector: (T) -> R): T? {
val iterator = iterator()
if (!iterator.hasNext()) return null
var maxElem = iterator.next()
if (!iterator.hasNext()) return maxElem
var maxValue = selector(maxElem)
do {
val e = iterator.next()
val v = selector(e)
if (maxValue < v) {
maxElem = e
maxValue = v
}
} while (iterator.hasNext())
return maxElem
}
5.1.3 Lambda表达式的语法
{x:Int,y:Int -> x+y}
lambda表达式语法要点
- 始终用{}包围
- 箭头前部分是实参,实参没有用括号括起来
- 箭头后是函数体
- 可以把lambda表达式存储在一个变量中,把这个变量当做普通函数对待
val sum = {x:Int,y:Int -> x+y}
println(sum(1,2))
- 可以直接调用lambda表达式,有两种形式
{ println(42) }()
不直观,可读性差run{ println(42) }
- 如果lambda表达式是函数调用的最后一个实参,就可以把它放到括号的外边
people.maxBy() {p:Person -> p.age}
- 当lambda是函数唯一的实参时,还可以去掉代码中的空括号对
people.maxBy{p:Person -> p.age}
- 如果lambda参数的类型可以被推导出来,就可以去掉参数类型显示。当不能被推导时,就不能去掉。一个规则是先不声明类型,等编译器报错再指定它们。
people.maxBy{p -> p.age}
- 当实参名称没有显式地指定时,可以使用默认参数名称it代替
people.maxBy{it.age}
- it约定能大大缩短代码,但不应该滥用,最好显式声明每个参数,否则在一些情况,如嵌套lambda时难以搞清it引用的值。
- 同其他表达式一样,lambda表达式最后一行就是表达式的返回值
5.1.4 在作用域中访问变量
如果在函数内部使用lambda,lambda也可以访问这个函数的参数,还有在lambda之前定义的局部变量。
kotlin中不仅限于访问这些变量,还可以在lambda内部修改这些变量。Kotlin中称这些变量被lambda捕捉。
有几个注意的点:
- 局部变量的生命周期被限制在函数中,当被lambda捕捉时,这个变量的代码可以被存储并稍后执行。
- 原理:对于final变量,就是和lambda代码一起存储;非final变量,它的值被封装在一个特殊的包装器中。这个包装器的引用会和lambda代码一起存储。
- 如果lambda被用在异步执行的情况,如响应事件处理器,对局部变量的修改只会在lambda执行的时候发生。
5.1.4 成员引用
1.概念解释
创建一个调用单个函数或者单个属性的函数值。
2.表现形式:用双冒号把类名称与要引用的成员名称隔开,如Person::age。
一定注意后面是没有括号的
3.代码示例
val getAge = Person::age
//等同于
val getAge = {person:Person -> person.age}
//使用
people.maxBy(Person::age)
4.几个特殊情况
4.1引用顶层函数,去掉不存在的类名称
fun salute() = println("Salute!")
run(::salute)
4.2如果lambda被委托给一个接受多个参数的函数,提供成员引用会非常方便
val action = {person:Person, message:String -> sendEmail(person,message)}
val action = ::sendEmail
4.3可以用构造方法引用存储或者延期执行创建类实例的动作
val createPerson = ::Person
val p = createPerson("a",22)
println(p)
4.4同样适用于扩展函数
fun Person.isAdult() = age >= 21
val predicate = Person::isAdult
5.2 集合的函数式API
函数式编程风格在操作集合时提供了很多优势。下面这些函数并不是Kotlin设计者发明的,而是存在于所有支持lambda的语言中。
5.2.1 基础:filter和map
1.filter:遍历集合并选出应用给定lambda后会返回true的那些元素
示例1:筛选偶数
val list = listOf(1,2,3,4)
println(list.filter{it%2 == 0})
>>>>
[2,4]
示例2:筛选范围
val people = listOf(Person("a,"29),Person("b",31),Person("c",40))
println(peop;e.filter{it.age >30 })
>>>>
[Person(name=b,age=31),Person(name=c,age=40)]
2.map:对集合中的每一个元素应用给定的函数并把结果收集到一个新集合。
示例1:平方数
val list = listOf(1,2,3,4)
println(list.map{it*it})
>>>>>
[1,4,9,16]
示例2:打印名字
val people = listOf(Person("a,"29),Person("b",31),Person("c",40))
println(people.map{it.name })
>>>>
[a,b,c]
//可以用成员引用重写为
people.map(Person::name)
2.1.特别的四个函数
- filterKeys:根据key过滤map中的元素,it指key
- filterValues:根据value过滤map中的元素,it指value
- mapKeys:操作每一个元素里的key,it指每一对元素
- mapValues:操作每一个元素里的value,it指每一对元素
示例
val numbers = mapOf('a' to "aim",'b' to "bill",'c' to "coin")
println(numbers.mapKeys{it.key + 1})
println(numbers.mapValues{it})
println(numbers.filterKeys{it>'b'})
println(numbers.filterValues{it.length>3})
>>>>
{b=aim, c=bill, d=coin}
{a=a=aim, b=b=bill, c=c=coin}
{c=coin}
{b=bill, c=coin}
5.2.2 all、any、count和find:对集合应用判断式
1.概念说明
- all:是否所有元素都满足判断式
- any:集合中是否至少存在一个匹配的元素
- count:检查有多少元素满足判断式
- find:返回第一个符合条件的元素
2.示例
println(people.all{it.age >30 })
println(people.any{it.age >30 })
println(people.count{it.age >30 })
println(people.find{it.age >30 })
>>>>>
false
true
2
Person(name=b,age=31)
5.2.3 groupBy:把列表转换成分组的map
1.概念
把所有元素按照不同特征划分成不同的分组。
2.示例
val people = listOf(Person("a,"29),Person("b",31),Person("c",29))
println(people.groupBy{it.age })
>>>>
[29=[Person(name=a,age=29),Person(name=c,age=29)],
31=[Person(name=b,age=31)]
5.2.4 flatMap和flatten:处理嵌套集合中的元素
1.概念说明
- flatMap:首先根据作为实参给定的函数对集合中的每个元素做变换,然后把多个列表合并,重复元素依然出现。
- flatten:相对于flatMap来说就是没有变换,直接合并。但只能对元素是list的list进行。
2.示例
val strings = listOf("abc","cde","f")
println(strings.flatMap{it.toList()})
val strings2 = listOf("11","22","33")
val listOfLists = listOf(strings,strings2)
println(listOfLists.flatten())
>>>>>
[a, b, c, c, d, e, f]
[abc, cde, f, 11, 22, 33]
5.3 惰性集合操作:序列
1.产生背景
对集合进行链式函数调用时,这些函数会及早地创建中间集合。这些中间集合会被存储在临时列表中。当集合中数据较大时,计算性能和内存开销会受到很大影响。
2.惰性集合操作
kotlin的惰性集合操作入口就是Sequence接口。这个接口表示的就是一个可以逐个列举元素的序列。Sequence只提供了一个方法:iterator。
3.转换
调用扩展函数asSequence把任意集合转换成序列,调用toList进行反向转换。
5.3.1 执行序列操作:中间和末端操作
1.序列操作分为两种
- 中间操作:返回的是一个序列,始终是惰性的。
- 末端操作:返回的是一个结果,会触发执行所有的延期计算。
2.示例
listOf(1,2,3,4).asSequence()
.map{
println("map$it")
it*it
}
.filter{
println("filter$it")
it%2 == 0
}
.toList()
//在不加最后这个toList时,不会打印任何东西
>>>>
map1
filter1
map2
filter4
map3
filter9
map4
filter16
3.调用顺序
不同的调用顺序对链式调用影响很大,不论对于序列和list都是如此。
要认真考虑是否可以优化调用顺序,以优化计算效率。
如进行filter和map时,如果map前filter后,则一定会执行map对所有元素进行操作后再进行筛选,而先filter后map的话则会先筛选元素后进行map转换。
5.3.2 创建序列
除了在集合上调用asSequence外,还可以用generateSequence函数来创建序列。
1.给定序列中的前一个元素,计算下一个元素。
val numbers = generateSequence(0){it + 1}
val numbersTo100 = numbers.takeWhile{ it<=100 }
println(numbersTo100.sum())
>>>>
5050
2.父序列:元素的父元素和它的类型相同。如下面例子查询文件是否在隐藏目录中
fun File.isInsideHiddenDirectory() = generateSequence(this){ it.parentFile }.any{ it.isHidden }
val file = File("/use/dddd/.....")
println(file.isInsideHiddenDirectory())
5.4 使用Java函数式接口
两个概念注意
- 函数式接口:又称SAM接口,SAM为单抽象方法,即接口只有一个抽象方法。
- 函数类型:Java没有函数类型,Kotlin有完全的函数类型,需要接受lambda作为参数的是就是用函数类型。
- Kotlin不支持把lambda自动转换成实现Kotlin接口的对象。
- 在后面8.1章节会讨论函数类型声明的用法
5.4.1 把lambda当做参数传递给Java方法
1.可以在Kotlin中把lambda传给Java中任何期望函数式接口的方法。
2.通过显式地创建一个实现了接口匿名对象也能达到效果
3.上述两种的不同在于
- 使用lambda时,如果lambda没有访问任何来自定义它的函数的变量,相应的匿名类实例只会被创建一次,可以在多次调用之间重用。而如果lambda进行了外围作用域的变量捕捉,那么会在每次调用时创建一个新对象。
- 使用显式声明对象时,每次调用都会创建一个新的实例
4.上面所说的为lambda创建一个匿名类,是只适用于期望SAM接口的Java方法,对集合使用Kotlin扩展方法的方式无效。
5.示例:
/* Java */
void postCompute(int delay,Runnable computation)
/* Kotlin */
postCompute(1000){ println(42) }
//同样方式可行,但显式声明匿名对象会导致每次都创建新对象
postCompute(1000,object:Runnable{
override fun run(){
println(42)
}
})
//完全等价的方式
val runnable = Runable{ println(42) }
postCompute(1000,runnable)
5.4.2 SAM构造方法:显式地把lambda转换成函数式接口
1.概念说明
SAM构造方法:名称和函数式接口的名称一样,只接受一个参数,这个参数是被用作函数式接口单抽象方法的lambda。返回实现了这个接口的类的一个实例。
2.应用场景:编译器不会自动应用转换的时候
- 如果一个方法返回的是一个函数式接口的实例,不能直接返回一个lambda时。
- 需要把从lambda生成的函数式接口的类的实例存储在一个变量中时。
- 尽管方法转换中SAM转换一般会自动发生,但总会有编译器不能正确重载转换的情况,这时就需要用显式地SAM构造方法来解决错误。
3.示例
//应用1
fun createDoneRunnable():Runnable{
return Runnable{ println("done") }
}
createDoneRunnable().run()
//应用2
val listener = OnClickListener{ view -> when(view.id){ ..... } }
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)
5.5 带接受者的lambda:with和apply
5.5.1 with函数
1.结构说明
接受两个参数,第二个参数是个lambda,第一个参数会被转换作为第二个参数的接受者,可以显式地通过this引用访问这个接受者。当然,也可以省略this,直接访问这个值得方法和属性。
2.示例
//带this
fun alphabet():String{
val stringBuilder = StringBuilder()
return with(stringBuilder){
for(letter in "A".."Z"){
this.append(letter)
}
this.toString()
}
}
//不带this,且用表达式函数体
fun alphabet() = with(stringBuilder){
for(letter in "A".."Z"){
append(letter)
}
toString()
}
3.注意点
- lambda是一种类似普通函数的定义行为的方式,而带接收者的lambda是类似扩展函数的定义行为的方式。
- 当方法名称冲突时,可以用this加上显式标签,如想引用的是外部类的toString方法时
this@OuterClass.toString()
5.5.2 apply函数
1.结构说明
apply函数被声明成一个扩展函数,在Kotlin中可以在任意对象上使用,不需要特别支持。
with函数返回的是执行lambda代码的结果,apply始终会返回接受者对象。
2.示例
//重构上面例子
fun alphabet() = StringBuilder().apply{
for(letter in "A".."Z"){
append(letter)
}
}.toString()
//初始化TextView,也可以把setPadding放进括号里
TextView(context).apply{
text = "Sample Text"
textSize = 20.0
}.setPadding(10,0,0,10)
5.6 小结
略