写在开头:本人打算开始写一个Kotlin系列的教程,一是使本身记忆和理解的更加深入,二是能够分享给一样想学习Kotlin的同窗。系列文章的知识点会以《Kotlin实战》这本书中顺序编写,在将书中知识点展现出来同时,我也会添加对应的Java代码用于对比学习和更好的理解。express
Kotlin教程(一)基础
Kotlin教程(二)函数
Kotlin教程(三)类、对象和接口
Kotlin教程(四)可空性
Kotlin教程(五)类型
Kotlin教程(六)Lambda编程
Kotlin教程(七)运算符重载及其余约定
Kotlin教程(八)高阶函数
Kotlin教程(九)泛型编程
Lambda表达式,或简称lambda,本质上级就是能够传递给其余函数的一小段代码。有了lambda,能够轻松地把通用的代码结构抽取成库函数,Kotlin标准库就大量地使用了它们。bash
把lambda引入Java 8是Java这门语言演变过程当中让人望眼欲穿的变化之一。为何它是如此重要?这一节中,你会发现为什么lambda这么好用,以及Kotlin的lambda语法看起来是什么样子的。app
在你代码中存储和传递一小段行为是常有的任务。例如,你经常须要表达像这样的想法:“当一个时间发生的时候运行这个事件处理器”又或者是“把这个操做应用到这个数据接口中全部元素上”。在老版本的Java中,可使用匿名内部类来实现。这种技巧能够工做可是语法太啰嗦了。
函数式编程提供了另一种解决问题的方法:把函数当作值来对待。能够直接传递函数,而不须要先声明一个类再传递这个类的实例。使用lambda表达式以后,代码会更加简洁。都不须要声明函数了,能够高效地直接传递代码块做为函数参数。
咱们来看一个例子。假设你要定义一个点击按钮的行为,添加一个负责处理点击的监听器。监听器实现了相应的接口OnClickListener和它的一个方法onClick:异步
/* Java */
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//do something
}
});
复制代码
这样声明匿名内部类的写法实在是太啰嗦了。在Kotlin中咱们能够像Java 8同样使用lambda来消除这些冗余代码。ide
/* Kotlin */
button.setOnClickListener{ /* do someting */ }
复制代码
这段代码作了与上面一样的事情,可是不用再写啰嗦的匿名内部类了。
以前也说过Kotlin可使用关键字object
匿名内部类,所以,你想写成普通的方式也是能够的:函数式编程
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
println("on click")
}
})
复制代码
上面两种方式转换成Java代码:函数
button.setOnClickListener((OnClickListener)null.INSTANCE);
button.setOnClickListener((OnClickListener)(new OnClickListener() {
public void onClick(@Nullable View v) {
String var2 = "on click";
System.out.println(var2);
}
}));
复制代码
匿名内部类转换成了Java的匿名内部类。可是lambda应该是Kotlin本身作了特出处理,没法转换成相应的Java代码。post
咱们先来看一个例子,你会用到一个Person类,它包含这我的的名字和年龄信息:性能
data class Person(val name: String, val age: Int)
复制代码
假设如今你有一我的的列表,须要找到列表中年龄最大的那我的。若是彻底不了解lambda,你可能会这样作:
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("Hubert", 26))
>>> findTheOldest(people)
Person("Alice", 29)
复制代码
能够完成目的,可是代码稍微有点多。而Kotlin有更好的方法,可使用库函数:
>>> val people = listOf(Person("Alice", 29), Person("Hubert", 26))
>>> println(people.maxBy{ it.age })
Person("Alice", 29)
复制代码
maxBy函数能够在任何集合上调用,且只须要一个实参:一个函数,指定比较哪一个值来找到最大元素。花括号中的代码{ it.age }
就是实现了这个逻辑的lambda。它接收一个集合中的元素做为实参(使用it引用它)而且返回用来比较的值。这个例子中,集合元素是Person对象,用来比较的是存储在其age属性中的年龄。
若是lambda恰好是函数或者属性的委托,能够用成员引用替换:
people.maxBy{ Person::age }
复制代码
虽然lambda看上去很简洁,可是你可能不是很明白究竟是如何写lambda,以及里面的规则,咱们来学习下lambda表达式的语法吧。
一个lambda把一小段行为进行编码,你能把它当作值处处传递。它能够被独立地声明并存储到一个变量中。可是更常见的仍是直接声明它并传递给函数。
//参数 //函数体
{ x: Int, y: Int -> x + y }
复制代码
Kotlin的lambda表达式始终用花括号包围。->
把实参和函数体分割开,左边是参数列表,右边是函数体。注意参数并无用()
括起来。
能够把lambda表达式存储在一个变量中,把这个变量当作普通函数对待(即经过相应实参调用它):
>>> val sum = {x:Int,y:Int -> x + y}
>>> println(sum(1, 2))
3
复制代码
若是你乐意,还能够直接调用lambda表达式:
>>> { println(42) }()
42
复制代码
可是这样的语法毫无可读性,也没有什么意义(它等价于直接执行lambda函数体中的代码)。若是你确实须要把一小段代码封闭在一个代码块中,可使用库函数run来执行传递它的lambda:
>>> run{ println(42) }
42
复制代码
在以后的章节咱们会了解到这种调用和内建语言结构同样高效且不会带来额外运行时开销,以及背后的缘由。如今咱们继续看“找到列表中年龄最大”的例子:
>>> val people = listOf(Person("Alice", 29), Person("Hubert", 26))
>>> println(people.maxBy{ it.age })
Person("Alice", 29)
复制代码
若是不用任何简明语法来重写这个例子,你会获得下面的代码:
people.maxBy({ p: Person -> p.age })
复制代码
这段代码一目了然:花括号中的代码片断是lambda表达式,把它做为实参传给函数。这个lambda接收一个类型为Person的参数并返回它的年龄。
可是这段代码有点啰嗦。首先,过多的标点符号破坏了可读性。其次,类型能够从上下文推断出来并能够省略。最后,这种状况下不须要给lambda的参数分配一个名称。
让咱们来改进这些地方,先拿花括号开刀。Kotlin有这样一种语法约定,若是lambda表达式是函数调用的最后一个实参,它能够放到括号的外边。这个例子中,lambda是惟一的实参,因此能够放到括号的后边:
people.maxBy() { p:Person -> p.age }
复制代码
当lambda时函数惟一的实参时,还能够去掉调用代码中的空括号:
people.maxBy { p:Person -> p.age }
复制代码
三种语法形式含义都是同样的,但最后一种最易读。若是lambda是惟一的实参,你固然愿意在写代码的时候省掉这些括号。而当你有多个实参时,便可以把lambda留在括号内来强调它是一个实参,也能够把它放在括号的外面,两种选择都是可行的。若是你想传递两个更多的lambda,不能把超过一个lambda放在外面。
咱们来看看这些选项在更复杂的调用中是怎样的。还记得外面在教程二中定义的joinToString函数吗?Kotlin标准库中也有定义它,不一样之处在于它能够接收一个附加的函数参数。这个函数能够用toString函数之外的方法来把一个元素转换成字符串。下面的例子展现了你能够用它只打印出人的名字:
>>> val names = people.joinToString(separator = " ", transform = { p: Person -> p.name })
>>> println(names)
Alice Hubert
复制代码
这种方式使用命名实参来传递lambda,清楚地表示了lambda应用到了哪里。
下面的例子展现课能够怎样重写这个调用,把lambda放在括号外:
>>> val names = people.joinToString(" ") { p: Person -> p.name }
>>> println(names)
Alice Hubert
复制代码
这种方式没有显式地代表lambda引用到了哪里,因此不熟悉被调用函数的那些人可能更难理解。
在as或者IDEA中可使用Alt+Enter唤起操做,使用“Move lambda expression out of parentheses ”把lambda表达式移动到括号外,或“Move lambda expression into parentheses”把lambda表达式移动到括号内。
咱们继续简化语法,移除参数的类型。
people.maxBy { p:Person -> p.age }
people.maxBy { p -> p.age } //推导出参数类型
复制代码
和局部变量同样,若是lambda参数的类型能够被推导出来,你就不须要显示地指定它。以这里的maxBy函数为例,其参数类型始终和集合的元素类型相同。编译器知道你是对一个Person对象的集合调用maxBy函数,因此它能推导lambda参数也会是Person类型。
也存在编译器不能推断出lambda参数类型的状况,但这里咱们暂不讨论。能够遵循这样的一条简单的规则:先不声明类型,等编译器报错后再来指定它们。
这个例子你能作的最后简化是使用默认参数名称it代替命名参数。若是当前上下文指望的是只有一个参数的lambda且这个参数的类型能够推断出来,就会生成这个名称。
people.maxBy { it.age } //it是自动生成的参数名称
复制代码
仅实参名称没有显示地指定时这个默认的名称才会生成。
it约定能大大缩短你的代码,但你不该该滥用它。尤为是在嵌套lambda的状况下。最好显式地声明每一个lambda的参数。fouz,很难搞清楚it引用的究竟是那个值。若是上下文中参数的类型或意义都不是很明朗,显式声明参数的方法也颇有效。
若是你用变量存储lambda,那么就没有能够推断出参数类型的上下文,因此你必须显式地指定参数类型:
>>> val getAge = { p:Person -> p.age }
>>> people.maxBy(getAge)
复制代码
至此你看到的例子都是单个表达式或语句构成的lambda。可是lambda并无 被限制在这样小的规模,它能够包含更多的语句。下面这种状况,最后一个表达式就是(lambda的)结果:
val sum = { x: Int, y: Int ->
println("Computing the sum of $x and $y ...")
x + y
}
>>> println(sum(1, 2))
Computing the sum of 1 and 2 ...
3
复制代码
当在函数内声明一个匿名内部类的时候,可以在这个匿名内部类引用这个函数的参数和局部变量。也能够用lambda做一样的事情。若是在函数内部使用lambda,也能够访问这个函数的参数,还有在lambda以前定义的局部变量。
咱们用标准库函数forEach来展现这种行为。这个函数可以遍历集合中的每个元素,并在该元素上调用给定的lambda。forEach函数只是比普通for循环更简洁一些。
fun printMessageWithPrefix(message: Collection<String>, prefix: String) {
//接受lambda做为实参指定对每一个元素的操做
message.forEach {
println("$prefix $it") //在lambda中访问prefix参数
}
}
>>> val errors = listOf("403 Forbidden", "404 Not Found")
>>> printMessageWithPrefix(errors, "Error:")
Error: 403 Forbidden
Error: 404 Not Found
复制代码
这里Kotlin和Java的一个显著区别是:在Kotlin中不会仅限于访问final变量,在lambda内部也能够修改这些变量:
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")
}
>>> val response = listOf("200 OK", "418 I'm a teapot", "500 Internal Server Error")
>>> printProblemCounts(response)
1
复制代码
和Java不同,Kotlin容许在lambda内部访问非final变量甚至修改它们。从lambda内访问外部变量,咱们称这些变量被lambda捕获,就像这个例子中的prefix、clientErrors以及serverErrors同样。
访问非final变量甚至修改它们的原理
注意,默认状况下,局部变量的生命周期被限制在声明这个变量的函数中,可是若是它被lambda捕获了,使用这个变量的代码能够被存储并稍后再执行。你可能会问这事什么原理?当你捕获final变量时,它的值和使用这个值的lambda代码一块儿存储。而对非final变量来讲,它的值被封装在一个特殊的包装器中,这样你就能够改变这个值,而对这个包装器的引用会和lambda代码一块儿存储。
这个原理我在教程三中的匿名内部类中也有提到:访问建立匿名内部类的函数中的变量是没有限制在final变量,当时举了这个例子:
var clickCount = 0
B().setListener(object : Listener {
override fun onClick() {
clickCount++ //修改变量
}
})
复制代码
而且转换成了Java代码:
final IntRef clickCount = new IntRef();
clickCount.element = 0;
(new B()).setListener((Listener)(new Listener() {
public void onClick() {
int var1 = clickCount.element++;
}
}));
复制代码
能够看到真实被使用clickCount是int类型数,但在Java中使用确实包装类IntRef,而真实int变成了clickCount.element。
任什么时候候你捕获了一个final变量(val),它的值被拷贝下来,这和Java同样。而当你捕获了一个可变变量(var)时,它的值被做为Ref类的一个实例被存储下来。Ref变量是final的能轻易被捕获,然而实际值被存储在其字段中,而且能够在lambda内修改。
这里有个重要的注意事项,若是lambda被用做事件处理器或者用在其余异步执行的状况,对局部变量的修改只会在lambda执行的时候发生。例以下面这段代码并非记录按钮点击次数的正确方法:
fun tryToCountButtonOnClicks(button: Button): Int {
var clicks = 0
button.setOnClickListener { clicks++ }
return clicks
}
复制代码
这个函数始终返回0。尽管onClick处理器能够修改clicks的值,你并不能观察到值发生了变化,由于onClick处理器是在函数返回以后调用的。这个函数正确的实现须要把点击次数存储在函数外依然能够访问的地方——例如类的属性,而不是存储在函数的局部变量中。
你已经看到lambda是如何让你把代码块做为参数传递给函数的。可是若是你想要当作参数传递的代码已经被定义成了函数,该怎么办?固然能够传递一个调用这个函数的lambda,但这样作有点多余。name你能直接传递函数吗?
Kotlin和Java 8同样,若是把函数转换成一个值,你就能够传递它。使用::
运算符来转换:
val getAge = Person::age
复制代码
这种表达式称为成员引用,它提供了简明语法,来建立一个调用单个方法或者访问单个属性的函数值。双冒号把类名称与你要引用的成员(一个方法或者一个属性)名称隔开。
一样的内容用lambda表达式实现是这样的:
val getAge = { person: Person -> person.age }
复制代码
无论你引用的函数仍是属性,都不要在成员引用的名称后面加括号。成员引用和调用该函数的lambda具备同样的类型,因此能够互换使用。
还能够引用顶层函数(不是类的成员):
fun salute() = println("Salute!")
>>> run(::salute)
Salute!
复制代码
这种状况下,你省略了类名称,直接以::
开头。成员引用::salute
被当作实参传递库函数run,它会调用相应的函数。
若是lambda要委托给一个接收多个参数的函数,提供成员引用代替它将会很是方便:
val action = { person: Person, message: String ->
sendEmail(person, message) //这个lambda委托给sendEmail函数
}
val nextAction = ::sendEmail //能够用成员引用代替
复制代码
能够用构造方法引用存储或者延期执行建立类实例的做用。构造方法引用的形式是在双冒号后指定类名称:
data class Person(val name: String, val age: Int)
>>> val createPerson = ::Person
>>> val p = createPerson("Hubert", 26) //建立Person实例的动做被保存成了值
>>> println(p)
Person(name=Hubert, age=26)
复制代码
还能够用一样的方式引用扩展函数:
fun Person.isAdult() = age >= 18
val predicate = Person::isAdult
复制代码
尽管isAdult不是Person类的成员,仍是能够经过引用访问它,这和访问实例的成员没什么两样:person.isAdult()
。
绑定引用
在Kotlin 1.0 中,当接受一个类的方法或属性引用时,你始终须要提供一个该类的实例来调用这个引用。Kotlin 1.1 计划支持绑定成员引用,它容许你使用成员引用语法捕获特定实例对象上的方法引用。
>>> val p = Person("Hubert", 26)
>>> val personsAgeFunction = Person::age
>>> println(personsAgeFunction(p))
26
>>> val hubertsAgeFunction = p::age //Kotlin 1.1 中可使用绑定成员引用
>>> println(hubertsAgeFunction())
26
复制代码
注意personsAgeFunction是一个单参数函数(返回给定人的年龄),而hubertsAgeFunction是一个没有参数的函数(返回p对象的年龄)。在Kotlin 1.1 以前,你须要显式地写出lambda{ p.age }
,而不是使用绑定成员引用p::age
。
函数式编程风格在操做集合时提供了不少优点。大多数任务均可以经过库函数完成,来简化你的代码。
filter和map函数造成了集合操做的基础,不少集合操做都是借助它们来表达的。
每一个函数咱们都会给出两个例子,一个使用数字,另外一个使用熟悉的Person类:
data class Person(val name: String, val age: Int)
复制代码
filter函数遍历集合并选出应用给定lambda后会返回true的那些元素:
>>> val list = listOf(1, 2, 3, 4)
>>> println(list.filter { it % 2 == 0 }) //只留下偶数
[2, 4]
复制代码
上面的结果是一个新的集合,它只包含输入集合中那些知足判断是的元素。
若是你想留下那些超过30岁的人,能够用filter:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.filter { it.age > 30 })
Person(name=Bob, age=31)
复制代码
filter函数能够从集合中移除你不想要的元素,可是它并不会改变这些元素。元素的变换是map的用武之地。
map函数对集合中的每个元素应用给定的函数并把结果收集到一个新集合。能够把数字列表变换成它们平方的列表,好比:
>>> val list = listOf(1, 2, 3, 4)
>>> println(list.map { it * it })
[1, 4, 9, 16]
复制代码
结果是一个新集合,包含的元素个数不变,可是每一个元素根据给定的判断式作了变换。
若是你想打印的只是一个姓名的列表,而不是人的完整信息列表,能够用map来变换列表:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.map { it.name })
[Hubert, Bob]
复制代码
这个例子也能够同成员引用漂亮地重写:
people.map(Person::name)
复制代码
能够轻松地把屡次这样的调用连接起来。例如,打印出年龄超过30岁的人的名字:
>>> people.filter { it.age > 30 }.map(Person::name)
[Bob]
复制代码
如今,若是说须要这个分组中全部年龄最大的人的名字,能够先找到分组中人的最大年龄,而后返回全部这个年龄的人,很容易就用lambda写出以下代码:
people.filter { it.age == people.maxBy(Person::age).age }
复制代码
可是注意,这段代码对每一个人都会重复寻找最大年龄的过程,假设集合中有100我的,寻找最大年龄的过程就会执行100遍!下面的解决方法作出了改进,只计算了一次最大年龄:
val maxAge = people.maxBy(Person::age).age
people.filter { it.age == maxAge }
复制代码
若是没有必要就不要重复计算!使用lambda表达式的代码看起来简单,有时候却掩盖底层操做的复杂性。始终牢记你写的代码在干什么。
还能够对map集合应用过滤和变换函数:
>>> val numbers = mapOf(0 to "zero", 1 to "one")
>>> println(numbers.mapValues { it.value.toUpperCase() })
{0=ZERO, 1=ONE}
复制代码
键和值分别由各自的函数来处理。filterKeys和mapKeys过滤和变换map集合的键,而另外的filterValues和mapValues过滤和变换对应的值。
另外一种常见的任务是检查集合中全部元素是否都符合某个条件(或者它的变种,是否存在符合的元素)。Kotlin中,它们是经过all和any函数表达的。count函数检查有多少元素知足判断式,而find函数返回第一个符合条件的元素。
为了演示这些函数,咱们先来定义一个判断式,来检查一我的是否尚未到28岁:
val canBeInClub27 = { p:Person -> p.age <= 27 }
复制代码
若是你对是否全部元素都知足判断式感兴趣,应该使用all函数:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.all(canBeInClub27))
false
复制代码
若是你须要检查集合中是否至少存在一个匹配的元素,那就用any:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.any(canBeInClub27))
true
复制代码
注意,!all(不是全部)加上某个条件,能够用any加上这个条件的取反来替换,反之亦然。为了让你的代码更容易理解,应该选择前面不须要否认符号的函数:
>>> val list = listOf(1, 2, 3)
>>> println(!list.all { it == 3 }) //!否认不明显,这种状况最好使用any
true
>>> println(list.any { it != 3 }) //lambda参数中的条件要取反
true
复制代码
第一行检查是保证不是全部元素都等于3.这和至少有一个元素不是3是一个意思,这正式你在第二行用any作的检查。
若是你想知道有多少个元素知足了判断式,使用count:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.count(canBeInClub27))
1
复制代码
使用正确的函数完成工做:count VS size
count方法容易被遗忘,而后经过过滤集合以后再取大小来实现它:
>>> println(people.filter(canBeInClub27).size)
1
复制代码
在这种状况下,一个中间集合会被建立并用来存储全部知足判断式的元素。而另外一方面,count方法只是跟踪匹配元素的数量,不关心元素自己,因此更高效。
要找到一个知足判断式的元素,使用find函数:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.find(canBeInClub27))
Person(name=Hubert, age=26)
复制代码
若是有多个匹配的元素就返回其中第一个元素;或者返回null,若是没有一个元素能知足判断式。find还有一个同义方法firstOrNull,可使用这个方法更清楚地表达你的意图。
假设你须要把全部元素按照不一样的特征划分红不一样的分组。例如,你想把人按年龄分组,相同的年龄的人在一组。把这个特征直接当作参数传递十分方便。groupBy函数能够帮你作到这一点:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31), Person("Carol", 31))
>>> println(people.groupBy { it.age })
复制代码
此次操做的结果是一个map,是元素分组依据的键(这个例子中是age)和元素分组(persons)之间的映射:
{26=[Person(name=Hubert, age=26)], 31=[Person(name=Bob, age=31), Person(name=Carol, age=31)]}
复制代码
每个分组都是存在一个列表中,结果的类型就是Map<Int, List<Person>>
。可使用像mapKeys和mapValues这样的函数对这个map作进一步修改。
咱们再来看另一个例子,如何使用成员引用把字符串按照首字母分组:
>>> val list = listOf("a", "ab", "b")
>>> println(list.groupBy(String::first))
{a=[a, ab], b=[b]}
复制代码
这里的first并非String类的成员,而是一个扩展,也能够把它当作成员引用访问。
假设你有一堆书,使用Book类表示:
data class Book(val title: String, val authors: List<String>)
复制代码
每本书均可能有一个或者多个做者,能够统计出图书馆中的全部做者的set:
books,flatMap { it.authors }.toSet()
复制代码
flatMap函数作了两件事:首先根据做为实参给定的函数对集合中每一个元素作变换(或者说映射),而后把多个列表合并(或者说平铺)成一个列表。下面这个字符串的例子很好地阐明了这个概念:
>>> val strings = listOf("abc", "def")
>>> println(strings.flatMap { it.toList() })
[a, b, c, d, e, f]
复制代码
字符串上的toList函数把它转换成字符串列表。若是和toList一块儿使用的是map函数,你会获得一个字符列表的列表,就如同下图的第二行。flapMap函数还会执行后面的步骤,并返回一个包含全部元素的列表。
让咱们回到书籍做者的例子,每一本数均可能有多位做者,属性book.authors存储了每本书籍的做者集合,flatMap函数把全部书籍做者合并成了一个扁平的列表。toSet调用移除告终果集合中的全部重复元素。
当你卡壳在元素集合不得不合并一个的时候,你可能会想起flapMap来。若是你不须要作任何变换,只是须要平铺一个集合,可使用flatten函数:listOfLists.flatten()
。
在上一节,你看到了许多链式结合函数调用的例子,好比map和filter。这些函数会及早地建立中间集合,也就是说每一步的中间结果都被存储在一个临时列表。序列给了你执行这些操做的另外一种悬着,能够避免建立这些临时中间对象。
先来看个例子:
people.map(Person::name).filter { it.startsWith("A") }
复制代码
Kotlin标准库参考文档有说明,filter和map都会返回一个列表。这意味着上面例子中的链式调用会建立两个列表:一个保存filter函数的结果,另外一个保存map函数的结果。若是原列表只有两个元素,这不是什么问题,可是若是有一百万个元素,链式调用就会变得十分低效。
为了提升效率,能够把操做变成使用序列,而不是直接使用集合:
people.asSequence() //把初始集合转换成序列
.map(Person::name)
.filter { it.startsWith("A") }
.toList() //把结果序列转换回列表
复制代码
应用此次操做后的结果和前面的例子如出一辙:一个以字母A开头的人名列表。可是第二个例子没有建立任何用于存储元素的中间集合,因此元素数量巨大的状况下性能将显著提高。
Kotlin的惰性集合操做的入口就是Sequence接口。这个接口表示的就是一个能够逐个列举元素的元素序列。Sequence只提供了一个方法:iterator,用来从序列中获取值。
Sequence接口的强大之处在于其操做的实现方式。序列中的元素求值是惰性的。所以,可使用序列更高效地对集合元素执行链式操做,而不须要穿件额外的集合来保存过程当中产生的中间结果。
能够调用扩展函数asSequence把任意集合转换成序列,调用toList来作反向的转换。
序列操做分为两类:中间的和末端的。一次中间操做返回的时另外一个序列,这个新序列知道如何变换原始序列中的元素。而一次末端操做返回的是一个结果,这个结果多是集合、元素、数字,或者其余从初始集合的变换序列中获取的任意对象。
//中间操做 //末端操做
people.asSequence().map{..}.filter {..}.toList()
复制代码
中间操做始终都是惰性的。先看看下面这个缺乏了末端操做的例子:
>>> listOf(1, 2, 3, 4).asSequence()
.map { print("map($it)"); it *it }
.filter { print("filter($it)");it % 2 == 0 }
复制代码
执行这段代码并不会再控制台上输出任何内容。这意味着map和filter变换被延期了,它们只有在获取结果的时候才会被应用(即末端操做调用的时候):
>>> listOf(1, 2, 3, 4).asSequence()
.map { print("map($it)"); it *it }
.filter { print("filter($it)");it % 2 == 0 }
.toList()
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)
复制代码
末端操做触发执行了全部的延期计算。
这个例子中另一件值得注意的事情是计算执行的顺序。一个笨办法是如今每一个元素上调用map函数,而后在结果序列的每一个元素上在调用filter函数。map和filter对集合就是这样作的,而序列不同。对序列来讲,全部操做是按顺序应用在每个元素上的,处理完第一个元素(先映射在过滤),而后完成第二个元素的处理,以此类推。
这种方法意味着部分元素根本不会发生任何变换,若是在轮到它们以前就已经取得告终果。咱们来看一个map和find的例子。首先把一个数字映射成它的平方,而后找到第一个比数字3大的条目:
>>> println(listOf(1, 2, 3, 4)
.map { print("map($it); "); it * it }
.find { print("$it > 3 ?; "); it > 3 })
>>> println("----------------")
>>> println(listOf(1, 2, 3, 4).asSequence()
.map { print("map($it); "); it * it }
.find { print("$it > 3 ?; "); it > 3 })
map(1); map(2); map(3); map(4); 1 > 3 ?; 4 > 3 ?; 4
----------------
map(1); 1 > 3 ?; map(2); 4 > 3 ?; 4
复制代码
第一种状况,当你使用集合的时候,列表被变换成了另外一个lieb,因此map变换应用戴每个元素上,包括了数字3和4.而后第一个知足判断式的元素被找到了:数字2的平方。
第二种状况,find调用一开始就逐个地处理元素。从原始序列中取一个数字,用map变换它,而后在检查它是知足传给find的判断式。当进行到数字2时,返现它的平方已经比3大,就把它做为find操做结果返回了。再也不须要继续检查数字3和4,由于这以前你已经找到结果。
在集合上执行操做的顺序也会影响性能。假设你有一个集合,想要打印集合中哪些长度小于某个限制的人名。你须要作两件事:把每一个人映射成他们的名字,而后过滤掉其中哪些不够短的名字。这种状况能够用任何顺序应用map和filter操做。两种顺序获得的结果同样,但他们应该执行的变化总次数不同的:
>>> val people = listOf(Person("Hubert", 26), Person("Alice", 29),
Person("Bob", 31), Person("Dan", 21))
>>> println(people.asSequence()
.map { print("map(${it.name}); "); it.name }
.filter { print("filter($it); ");it.length < 4 }
.toList())
>>> println("----------------")
>>> println(people.asSequence()
.filter { print("filter(${it.name}); ");it.name.length < 4 }
.map { print("map($it); "); it.name }
.toList())
map(Hubert); filter(Hubert); map(Alice); filter(Alice); map(Bob); filter(Bob); map(Dan); filter(Dan); [Bob, Dan]
----------------
filter(Hubert); filter(Alice); filter(Bob); map(Bob); filter(Dan); map(Dan); [Bob, Dan]
复制代码
能够看到,若是map在前,每一个元素都被变换。而若是filter在前,不合适的元素会被尽早地过滤掉且不会发生变换。
流 VS 序列 若是你很熟悉Java 8 中的流这个概念,你会发现序列就是它的翻版。Kotlin提供了这个概念本身的版本,缘由是Java 8的流并不支持哪些基于Java老版本的平台,例如Android。若是你的目标版本是Java 8,流提供了一个Kotlin集合和序列目前尚未实现的重要特性:在多个CPU上并行执行流操做(好比map和filter)的能力。能够根据Java的目标版本和你的特殊要求在流和序列之间作出选择。
前面的列表都是使用同一个方法建立序列:在集合上调用asSquence()
。另外一个可能性是使用generateSequence函数。给定序列中的前一个元素,这个函数会计算出下一个元素。下面这个例子就是如何使用generateSequence计算100之内全部天然数之和。
>>> val naturalNumbers = generateSequence(0) { it + 1 }
>>> val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
>>> println(numbersTo100.sum())
5050
复制代码
这个例子中的naturalNumbers和numbersTo100都是有延期操做的序列。这些序列中的实际数字直到你调用末端操做(这里是sum)的时候才会求值。
另外一种常见的用例是父序列。若是元素的父元素和它的类型相同(好比人类或者Java文件),你可能会对它全部祖先组成的序列的特质感兴趣。下面这个例子能够查询文件是否放在隐藏目录中,经过建立一个其父类目录的序列并检查每一个目录的属性来实现。
fun File.isInsideHiddenDirectory() =
generateSequence(this) { it.parentFile }.any{ it.isHidden}
>>> val file = File("/Users/svtk/.HiddenDir/a.txt")
true
复制代码
你生成一个序列,经过提供第一个元素和获取每一个后续元素的方式来实现。若是把any换成find,你还能够获得想要的那个目录(对象)。注意,使用序列容许你找到须要的目录以后当即中止遍历目录。
Kotlin的lambda也能够无缝地和Java API互操做。在文章开头,咱们就把lambda传给Java方法的例子:
/* Kotlin */
button.setOnClickListener{ /* do someting */ }
复制代码
Button类经过接收类型为OnClickListner的实参的setOnClickListener方法给按钮设置一个新的监听器,在Java(8以前)中咱们不得不建立一个匿名类来做为实参传递:
/* Java */
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//do something
}
});
复制代码
在Kotlin中,能够传递一个lambda,代替这个例子:
/* Kotlin */
button.setOnClickListener{ view -> ... }
复制代码
这个lambda用来实现OnClickListener,它有一个类型为View的参数,和onclick方法同样。
这种方法能够工做的缘由是OnClickListener接口只有一个抽象方法。这种接口被称为函数式接口,或者SAM接口,SAM表明单抽象方法。Java API 中随处可见像Runnable和Callable这样的函数式接口,以及支持它们的方法。Kotlin容许你在调用接收函数式接口做为参数的方法时使用lambda,来保证你的Kotlin代码即整洁又符合习惯。
和Java不一样,Kotlin拥有彻底的函数类型。正由于这样,须要接收lambda做为参数的Kotlin函数应该使用函数类型而不是函数式接口类型,做为这些参数的类型。Kotlin不支持把lambda自动转换成Kotlin接口对象。咱们会在以后的章节中讨论声明函数类型的用法。
是否是很是好奇把lambda传给Java时到底发生了什么,是如何衔接的?
能够把lambda传给任何指望函数式接口的方法。例以下面这个方法,它有一个Runnable类型的参数:
/* Java */
void postponeComputation(int delay, Runnable computation);
复制代码
在Kotlin中,能够调用它并把一个lambda做为实参传给它。编译器会自动把它转换成一个Runnable的实例。
postponeComputation(1000) { println(42) }
复制代码
当咱们说一个Runnable的实例时,指的是一个实现了Runnable接口的匿名内部类的实例。编译器会帮你建立它,并使用lambda做为单抽象方法的方法体。
经过显示的建立一个实现了Runnable的匿名对象也能达到一样的效果:
postponeComputation(1000, object: Runnable {
override fun run() {
println(42)
}
})
复制代码
可是这里有一点不同。当你显式地声明对象时,每次调用都会建立一个新的实例。使用lambda的状况不一样:若是lambda没有访问任何来自定义它的函数的变量,相应的匿名类实例能够在屡次调用之间重用。
所以彻底等价的实现应该是下面这段代码中显示object声明,它把Runnable实例存储在一个变量中,而且每次调用的时候都使用这个变量:
val runnable = Runnable { println(42) }
fun handleComputation() {
postponeComputation(1000, runnable)
}
复制代码
若是lambda从包围它的做用域中捕获了变量,每次调用就再也不可能重用一同一个实例了。这种状况下,每次调用时编译器都要建立一个新对象,其中存储着被捕获的变量的值。
fun handleComputation(id: String) { //lambda会捕获id这个变量
postponeComputation(1000) { println(id) } //每次都建立一个Runnable新实例
}
复制代码
Lambda的实现细节
自Kotlin 1.0起,每一个lambda表达式都会被编译成一个匿名类,除非它是一个内联lambda。后续版本计划支持生成Java 8字节码。一旦实现,编译器就能够避免为每个lambda表达式都生成一个独立的.class文件。若是lambda捕获了变量,每一个被捕获的变量会在匿名类中有对应的字段,并且每次调用都会建立一个这个匿名 类的新实例。不然,一个单例就会被建立。类的名称由lambda声明所在的函数名称加上后缀衍生出来:上面一个例子就是HandleComputation$1
。若是你反编译以前lambda表达式的代码,就会看到:
class HandleComputation$1(val id: String) : Runnable {
override fun run() {
println(42)
}
}
fun handleComputation(id: String) {
postponeComputation(100, HandleComputation$1(id))
}
复制代码
如你所见,编译器给每一个被捕捉的变量生成了一个字段和一个构造方法参数。
SAM构造方法是编译器生成的函数,让你执行从lambda到函数式接口实例的显式转换。能够在编译器不会自动应用转换的上下文中使用它。例如,若是有一个方法返回的时一个函数式接口的实例,不能直接返回一个lambda,要用SAM构造方法把它包起来:
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All Done!") }
}
>>> createAllDoneRunnable().run()
All Done!
复制代码
SAM构造方法的名称和底层函数式接口的名称同样。SAM构造方法只接收一个参数——一个被用做函数式接口单抽象方法体的lambda,并返回实现了这个接口的类的一个实例。
除了返回值外,SAM构造方法还能够用在须要把从lambda省城的函数式接口实例存储在一个变量中的状况。假设你要在多个按钮上重用同一个监听器,就像下面的代码同样:
val listener = OnClickListener { view ->
val text = when(view.id) {
R.id.button1 -> "First Button"
R.id.button2 -> "Second Button"
else -> "Unknown Button"
}
toast(text)
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)
复制代码
listener会检查哪一个按钮是点击事件源并做出相应的行为。可使用实现了OnClickListener的对象声明来定义监听器,可是SAM构造方法给你更简洁的选择。
Lambda和添加/移除监听器
注意lambda内部没有匿名对象那样的this:没有办法引用到lambda转换成的匿名类实例,从编译器的角度来看,lambda是一个代码块,不是一个对象,并且也不能把它当成对象引用。Lambda中的this指向的是包围它的类。
若是你的事件监听器在处理事件时还须要取消它本身,不能使用lambda这样作。这种状况使用实现了接口的匿名对象,在匿名对象内,this关键字指向该对象实例,能够把它传给移除监听器的API。
不少语法都有这样的语句,能够用它对同一个对象执行屡次操做,而不须要反复把对象的名称写出来。Kotlin也不例外,但它提供的是一个叫with的库函数,而不是某种特殊的语言结构。
要理解这种用法,咱们先看看下面这个例子,稍后你会用with来重构它:
fun alphabet(): String {
val result = StringBuilder()
for (letter in 'A'..'Z') {
result.append(letter)
}
result.append("\nNow I know the alphabet!")
return result.toString()
}
>>> println(alphabet)
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Now I know the alphabet!
复制代码
上面这个例子中,你调用了result实例上好几个不一样的方法,并且每次调用都要重复result这个名称。这里状况还不算太糟,可是若是你用到的表达式更长或者重复得更多,该怎么办?
咱们来看看使用with函数重写这段代码:
fun alphabet(): String {
val stringBuilder = StringBuilder()
return with(stringBuilder) { //指定接收者的值,你会调用它的方法
for (letter in 'A'..'Z') {
this.append(letter) //经过显式地this来调用接收者值得方法
}
append("\nNow I know the alphabet!") //this能够省略
this.toString() //从lambda返回
}
}
复制代码
with结构看起来像是一个特殊的语法结构,但它其实是一个接收两个参数的函数:这个例子中两个参数分别是stringBuilder和一个lambda。这里利用了把lambda放在括号外的约定,这样整个调用看起来就像是内建的语言功能。固然你也能够选择把它写成with(stringBuilder, {...})
。
with函数把它的第一个参数转换成第二个参数传给他的lambda的接收者。能够显式地经过this引用来访问这个接收者。或者能够省略this引用,不用任何限定符直接访问这个值得方法和属性。这个例子中this指向了stringBuilder,这是传给with的第一个参数。
带接收者的lambda和扩展函数
你可能回忆起曾经见过类似的概念,this指向的时函数接收者。在扩展函数体内部,this指向了这个函数的那个类型的实例,并且也能够被省略掉,让你直接访问接收者的成员。一个扩展函数某种意义上来讲就是带接收者的函数。
让咱们进一步重构初始的alphabet函数,去掉额外的stringBuilder变量:
fun alphabet(): String = with(StringBuilder()) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
toString()
}
复制代码
如今这个函数只返回一个表达式,因此使用表达式函数体语法重写了它。能够建立一个新的StringBuilder实例直接当作实参传给这个函数,而后在lambda中不须要显示地this就能够引用这个实例。
方法名冲突
若是你当作参数传给with的对象已经有这样的方法,该方法的名称和你正在使用with的类中的方法同样,怎么办?这种状况下,能够给this引用加上显式地标签来代表你要调用的时哪一个方法。假设函数的alphabet是类OuterClass的一个方法。若是你想引用的是定义在外部类的toString方法而不是StringBuilder,能够用下面这种语句:
this@OuterClass.toString()
复制代码
with返回的值是执行lambda代码的结果,该结果就是lambda中的最后一个表达式(的值)。但有时候你想返回的是接收者对象,而不是执行lambda的结果。这时apply库函数就派上用场了。
apply函数几乎和with函数如出一辙,惟一的区别是apply始终会返回做为实参传递给它的对象(接收者对象)。让咱们再一次重构alphabet函数,这一次用的是apply:
fun alphabet(): String = StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
复制代码
apply被声明成一个扩展函数,他的接收者编程了做为实参的lambda的接收者。执行apply的结果是StringBuilder,因此接下来你能够调用toString把它转换成String。
许多状况下apply都颇有效,其中一种是在建立一个对象实例须要用正确的方式初始化它的一些属性的时候。在Java中,这一般是经过另一个单独的Builder对象来完成的,而在Kotlin中,能够在任意对象上使用apply,彻底不须要任何任何来自定义该对象的库的特别支持。
咱们来用apply演示一个Android中建立TextView实例的例子:
fun createViewWithCustomAttr(context: Context) =
TextView(context).apply {
text = "Sample Text"
textSize = 20.0f
setPadding(10, 0, 0, 0)
}
复制代码
apply函数容许你使用紧凑的表达式函数体风格。新的TextView实例建立以后当即被传给了apply。在传给apply的lambda中,TextView实例变成了接收者,你就能够调用它的方法并设置它的属性。Lambda执行以后,apply返回已经初始化过的接收者实例,它变成了createViewWithCustomAttr函数的结果。
with函数和apply函数是最基本和最通用的使用带接收者的lambda的例子。更多具体的函数函数也可使用这种模式。例如,你可使用标准库函数buildString进一步简化alphabet函数,它会负责建立StringBuilder并调用toString:
fun alphabet(): String = buildString {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}
复制代码