5.Lambda 编程
Lambda表达式本质上就是可以传递给其他函数的一小段代码。
如果lambda刚好是函数或者属性的委托,可以用成员引用替换。
如果需要把一小段代码封闭在一个代码块中,可以使用库函数run
来执行传给它的lambda。
people.maxBy({p: Person -> p.age})
Kotlin有这样一种语法约定,如果lambda表达式是函数调用的最后一个实参,它可以放到括号的外边。
people.maxBy() {p: Person -> p.age}
当lambda时函数的唯一实参时,还可以去掉调用代码的空括号对:
people.maxBy{p: Person -> p.age}
和局部变量一样,如果lambda参数的类型可以被推导出来,就不需要显式地指定它。
people.maxBy { p -> p.age}
可以使用默认参数名称it
代替命名参数。如果当前上下文期望的事只有一个参数的lambda且这个参数的类型可以推导出来,就会生成这个名称。
people.maxBy{ it.age }
fun printProblemCounts(response: Collection<String>) {
var clientErrors = 0
var serverErrors = 0
response.forEach{
if (it.startsWith("4")){
clientErrors++
} else if (it.startsWith("5")) {
serverErrors++
}
}
println("$clientErrors client errors, $serverErrors server errors")
}
和Java不一样,Kotlin允许在lambda内部访问非final变量甚至修改他们。从lambda内访问外部变量,我们称这些变量被lambda捕捉。默认情况下,局部变量的生命期被限制在声明这个变量的函数中。但是如果它被lambda捕捉了,使用这个变量的代码可以被存储并稍后再执行。当你捕捉final变量时,它的值和使用这个值的lambda代码一起存储。而对非final变量来说,它的值被封装在一个特殊的包装器中,这样你就可以改变这个值,而对这个包装器的引用会和lambda代码一起存储。
Java只允许你捕捉final变量。当你想捕捉可变变量时,可以使用下面两种技巧:要么声明一个单元素的数组,其中存储可变值;要么创建一个包装类的实例,其中存储要改变的值的引用。
class Ref<T>(val value: T)
val counter = Ref(0)
val inc = { counter.value ++}
在kotlin中,任何时候你捕捉了一个final变量,它的值被拷贝下来,这和Java一样。而当你捕捉了一个可变变量时,它的值被作为Ref类的一个实例被存储下来。Ref变量是final的能轻易地被捕捉,然而实际值被存储在其字段中,并且可以在lambda内修改。
Kotlin和Java 8一样,如果把函数转换成一个值,你就可以传递它。使用::
运算符来转换。
val getAge = Person::age
这种表达式称为成员引用,它提供了简明的语法,来创建一个调用单个方法或者访问单个属性的函数值。双冒号把类名称与你要引用的成员(一个方法或者一个属性)名称隔开。
还可以引用顶层函数(不是类的成员)
fun salute() = println("salute")
>>> run(::salute)
可以用构造方法引用存储或者延期执行创建类实例的动作。构造方法引用的形式是在双冒号后指定类名称:
data class Person(val name: String, val age : Int)
>>> val createPerson = ::Person
>>> val p = createPerson("Alice", 28)
filter
函数可以从集合中移除你不想要的元素,但是它并不会改变这些元素。map
函数对集合中的每一个元素应用给定的函数并把结果收集到一个新集合。filterKeys
和mapKeys
过滤和变换map的键,filterValues
和mapValues
过滤和变换对应的值。
all
检查集合中的所有元素是否都符合某个条件。any
检查集合中是否存在符合的元素。count
函数检查有多少元素满足判断式,find
函数返回第一个符合条件的元素。
people.filter(canBeInClub27).size
在这种情况下,一个中间集合会被创建并用来存储所有满足判断式的元素。count
方法只是跟着匹配元素的数量,不关心元素本身,所以更高效。
groupBy
把列表转换成分组的map
flatMap
函数做了两件事情:首先根据作为实参给定的函数对集合中的每个元素做变换,然后把多个列表合并(或者说平铺)成一个列表。如果不需要做任何变换,只是需要平铺一个集合,可以使用flatten
函数。
class Book(val title: String,val authors: List<String>)
val strings = listOf("abc", "cde", "def", "defgh")
println(strings.flatMap { it.toList() })
val books = listOf(Book("Link", listOf("Solarex")), Book("Web", listOf("Solarex", "flyfire")), Book("Java", listOf("flyfire")))
println(books.flatMap { it.authors }.toSet())
var listOfLists = listOf(
listOf(1),
listOf(1,2,3),
listOf(3,4,5)
)
println(listOfLists.flatten())
map
和filter
等函数会及早地创建中间集合,也就是说每一步的中间结果都被存储在一个临时列表中。序列给了你执行这些操作的另一种选择,可以避免创建这些临时中间对象。为了提高效率,可以把操作变成使用序列,而不是直接使用集合:
people.asSequence().map(Person::name).filter{it.startsWith("A")}.toList()
Kotlin惰性集合操作的入口就是Sequence
接口,这个接口表示的就是一个可以逐个列举元素的元素序列。Sequence
只提供了一个方法,iterator
,用来从序列中获取值。Sequence
接口的强大之处在于其操作的实现方式,序列中的元素求值是惰性的。因此,可以使用序列更高效地对集合元素执行链式操作,而不需要创建额外的集合来保存过程中产生的中间结果。可以调用扩展函数asSequence
把任意集合转换成序列,调用toList
来做反向的转换。
listOf(1,2,3,4).map{ println("map $it"); it * it }.find{ println("find $it"); it > 3 }
listOf(1,2,3,4).asSequence().map{ println("map $it"); it * it }.find{ println("find $it"); it > 3 }
及早求值在整个集合上执行某个操作,惰性求值则逐个处理元素。
可以在集合上调用asSequence
来创建序列,也可以使用generateSequence
函数。给定序列中的前一个元素,这个函数会计算出下一个元素。
>>> val natualNumbers = generateSequence(0) { it+1 }
>>> val numbersTo100 = natualNumbers.takeWhile { it <= 100 }
>>> println(numbersTo100)
kotlin.sequences.TakeWhileSequence@af3b0db
>>> numbersTo100.sum()
5050
只有一个抽象方法的接口被称为函数式接口,或者SAM接口,SAM代表单抽象方法。
如果lambda没有访问任何来自定义它的函数的变量,相应的匿名类实例可以在多次调用之间重用。如果lambda从包围它的作用域中捕捉了变量,每次调用就不在可能重用同一个实例了。这种情况下,每次调用时编译器都要创建一个新对象,其中存储着被捕捉的变量的值。
自Kotlin1.0起,每个lambda表达式都会被编译成一个匿名类,除非它是一个内联lambda。如果lambda捕捉了变量,每个被捕捉的变量会在匿名类中有对应的字段,而且每次对lambda的调用都会创建一个这个匿名类的新实例。否则,一个单例就会被创建。类的名称有lambda声明所在的函数名称加上后缀衍生出来。
>>> val sum = {x:Int, y:Int -> x+y}
>>> println(sum.javaClass)
class Line_6$sum$1
注意lambda内部没有匿名对象那样的this:没有办法引用到lambda转换成的匿名类实例。从编译器的角度来看,lambda是一个代码块,不是一个对象,而且也不能把它当成对象引用。lambda中的this引用指向的是包围它的类。如果你的事件监听器在处理事件时还需要取消它自己,不能使用lambda这样做。这种情况使用实现了接口的匿名对象。在匿名对象内,this指向该对象实例,可以把它传递给移除监听器的API。
>>> val s = {x:Int, y:Int -> println(this.javaClass); x+y}
>>> s(1,2)
class Line_8
3
在lambda函数体内可以调用一个不同对象的方法,而且无须借助任何额外限定符;这种能力在Java中是找不到的。这样的lambda叫做带接收者的lambda。
lambda是一种类似普通函数的定义行为的方式,而带接收者的lambda事类似扩展函数的定义行为的方式。
apply
函数几乎和with
函数一模一样,唯一的区别是apply
始终返回作为实参传递给它的对象(i.e. 接收者对象)
apply
被声明成一个扩展函数,它的接收者编程了作为实参的lambda的接收者。