前言
Lambda 编程是 Kotlin 的灵魂所在,但 Lambda 的知识太多,现在就只讲讲 Lambda 编程的基础知识,带你入门。
集合的创建与遍历
集合的函数式API是用来入门 Lambda 编程的绝佳示例,不过我们先来学习创建集合的方式。
不可变集合
传统意义上,集合就分为两种:List 和 Set,再广泛一些,可以将 Map 这样的键值对结构也包含进来。
在 Java 中,List、Set 和 Map都是接口,List的主要实现类是ArrayList和LinkedList,Set的主要实现类是HashSet,Map的主要实现类是HashMap。
如果现在我们需要创建一个集合来存放数据,你可能会创建一个集合的实例,然后将数据一个个添加进去,像这样:
fun main() {
val list = ArrayList<String>()
list.add("Red")
list.add("Yellow")
list.add("White")
list.add("Cyan")
list.add("Green")
}
但这种初始化集合的方式比较繁琐,你可以使用 Kotlin 提供的 listOf() 函数来简化写法,如下所示:
fun main() {
val list = listOf(
"Red",
"Yellow",
"White",
"Cyan",
"Green",
)
}
可以看到只需一行代码,就完成了集合的初始化操作。
现在我们使用 for-in 循环来遍历我们刚刚创建的集合:
fun main() {
val list = listOf(
"Red",
"Yellow",
"White",
"Cyan",
"Green",
)
for (color in list){
println(color)
}
}
运行结果:
可变集合
前面使用 listOf() 函数创建的是一个不可变的集合。
什么意思呢?
就是集合只能用于读取,无法对集合进行添加、修改或删除元素的操作。这么设计的理由,和 val 关键字、类默认不可被继承的设计缘由是类似的,一切为了安全。
如果要创建一个可变的集合,也不难,使用 mutableListOf() 函数就可以了:
fun main() {
val list = mutableListOf(
"Red",
"Yellow",
"White",
"Cyan",
"Green",
)
list.add("Gray")
for (color in list) {
println(color)
}
}
运行结果:
可以看到新增的元素 “Gray” 颜色,已经被打印出来了。
Set 集合
Set 集合的用法和 List 集合几乎一模一样,但创建集合的函数变为了 setOf() 和 mutableSetOf()。代码如下:
fun main() {
val set = mutableSetOf(
"Red",
"Yellow",
"White",
"Cyan",
"Green",
)
set.add("Gray")
for (color in set) {
println(color)
}
}
运行结果:
注意:Set 集合的元素不会重复。具体来说,如果存放了多个相同的元素,只会保留较先存放的那个元素。
Map 集合
再来看看 Map 集合的用法,Map是一种键值对形式的数据结构。传统中,会创建一个 HashMap 的实例,然后将一个个键值对数据添加进去,比如:
fun main() {
val map = HashMap<String, Int>()
map.put("Red", 1)
map.put("Yellow", 2)
map.put("White", 3)
map.put("Cyan", 4)
map.put("Green", 5)
}
但 Kotlin 中并不建议使用 put() 和 get() 方法来对 Map 进行添加和读取操作,而是推荐通过类似数组索引的语法 [] 来访问和设置 Map 中的元素,比如:
fun main() {
val map = HashMap<String, Int>()
// 往 map 中添加数据
map["Red"] = 1
map["Yellow"] = 2
map["White"] = 3
map["Cyan"] = 4
map["Green"] = 5
// 从 map 中读取数据
val greenNumber = map["Green"]
println("the green number is $greenNumber ")
}
当然,这还不是最简便的写法,Kotlin 也提供了 mapOf() 和 mutableMapOf() 函数来初始化 Map 集合。我们在函数的参数中直接传入键值对组合就行了:
val map = mapOf("Red" to 1, "Yellow" to 2, "White" to 3, "Cyan" to 4, "Green" to 5)
键值对组合是通过 to 这个中缀函数来进行关联的,现在你只需知道它简化了如下函数调用的写法就行了:
"Red".to(1) // => "Red" to 1
我们再来遍历一下 Map 集合中的数据,依然使用 for-in 循环:
fun main() {
val map = mapOf("Red".to(1), "Yellow" to 2, "White" to 3, "Cyan" to 4, "Green" to 5)
for ((color,colorNumber) in map){
println("color is $color,color's number is $colorNumber")
}
}
与之前集合的遍历不同的是,我们这里利用了Kotlin的特性:解构声明,也就是将两个变量放到一个括号中,这样每次遍历的结果会赋给这两个变量,键会赋值给第一个变量,值会赋值给第二个变量。
运行结果:
学习完集合的创建与遍历之后,就正式开始学习集合的函数式API,入门 Lambda 编程。
集合的函数式API
我们主要学习函数式API的语法结构,也就是 Lambda 表达式的语法结构。
假如要找出集合中最长的字符串,你可能会这么写:
fun main() {
val list = listOf("zhangsan", "lisi", "wangwu")
var maxLengthName = ""
for (name in list) {
if (name.length > maxLengthName.length) {
maxLengthName = name
}
}
println("the max length name is $maxLengthName")
}
这样写,代码简洁也很有效,但使用集合的函数式API,可以进一步简化代码:
fun main() {
val list = listOf("zhangsan", "lisi", "wangwu")
val maxLengthName = list.maxBy { it.length }
println("the max length name is $maxLengthName")
}
这就是函数式API的用法。你一时理解不了是很正常的,学完 Lambda 表达式的语法结构你就会觉得简单易懂了。
Lambda表达式
先来看看 Lambda 表达式的定义,简单来说就是一段可以作为参数传递的代码,或者你可以说是“没名字”的函数。
为什么这么说呢?你看它的语法结构定义:
{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}
最外层是一对大括号,如果有参数需要传入到 Lambda 表达式,还需要声明参数列表。参数列表的结尾跟着 ->,表示参数列表的结束和函数体的开始,我们可以在函数体中编写代码,并且会将最后一行代码的执行结果自动返回。
简化 Lambda 表达式
很多时候我们并不会使用 Lambda 表达式完整的语法结构,而是会进行简化。
我们回到刚刚的示例,maxBy 函数接收的就是一个 Lambda 类型的参数,并且会将每次遍历到的值作为参数传递给 Lambda 表达式。它就是根据我们传入的 Lambda 表达式作为条件,从而找到该条件下的最大值。
理解了 maxBy 函数的工作原理后,我们就开始一步步推导如何简化Lambda表达式。首先是示例最完整的写法:
fun main() {
val list = listOf("zhangsan", "lisi", "wangwu")
val lambda = { name: String -> name.length }
val maxLengthName = list.maxBy(lambda)
println("the max length name is $maxLengthName")
}
首先我们可以去掉冗余的 lambda 变量,直接将 Lambda 表达式传入 maxBy 函数的参数中,第一步简化如下:
val maxLengthName = list.maxBy({ name: String -> name.length })
然后 Kotlin 规定,当 Lambda 参数是函数的最后一个参数时,可以将 Lambda 表达式移到函数的括号外面,放在函数的尾部,如下所示:
val maxLengthName = list.maxBy() { name: String -> name.length }
如果该 Lambda 参数是函数的唯一参数时,可以将函数的括号省略:
val maxLengthName = list.maxBy { name: String -> name.length }
由于 Kotlin 的类型推导机制,Lambda 表达式中的参数并不用声明类型,代码进一步简化:
val maxLengthName = list.maxBy { name -> name.length }
最后当 Lambda 表达式的参数列表只有一个参数时,可以不指定参数名,默认的参数名是 it。并且没有了参数名(也就没有了参数列表),不能也不需要使用 -> 来分隔参数列表和函数体了。那么代码就变为了:
val maxLengthName = list.maxBy { it.length }
这就是最开始的写法,怎么样?是不是印证了之前说的简单易懂。
常用的函数式API
学习完函数式API的语法结构后,再来看看集合中比较常用的函数式API。
map 函数
集合中的 map 函数可以将集合中的每个元素映射为另一个值,就是进行转换。转换的规则通过 Lambda 表达式进行指定,最终这个函数会将所有转换后的值装进一个集合中。
比如我们要让颜色名称变为大写,可以这样写:
fun main() {
val list = listOf("Red", "Yellow", "White", "Cyan", "Green")
val newList = list.map { it.uppercase() }
for (color in list){
println(color)
}
}
运行结果:
map函数的功能非常强大,它可以按照我们的需求对集合中的元素进行任意的映射转换,上面只是一个简单的示例而已。除此之外,还能完成更复杂的需求,只要在Lambda表示式中编写你需要的逻辑即可。
filter 函数
再来看看 filter 函数,它可用于过滤集合中的数据,比如我们可以将 filter 和 map 串联起来使用:
fun main() {
val list = listOf("Red", "Yellow", "White", "Cyan", "Green")
val newList = list
.filter { it.length <= 4 }
.map { it.uppercase() }
for (color in newList) {
println(color)
}
}
我们这里使用 filter 函数保留了字符串长度小于等于4的颜色名称,并且通过 map 函数将筛选出的元素转为了大写。
运行结果:
any 和 all 函数
any 函数用于判断集合中是否至少有一个元素满足指定条件,all 函数用于判断集合中是否所有元素都满足指定条件,满足的话返回 true。
代码如下所示:
fun main() {
val list = listOf("Red", "Yellow", "White", "Cyan", "Green")
val anyResult = list.any { it.startsWith("C") } // 是否有以 C 字母开头的颜色名称
val allResult = list.all { it.length >= 4 } // 是否所有颜色名称的长度都大于等于4
println("Is there a string starting with the C letter ? $anyResult")
println("Is all strings longer than or equal to 4 ? $allResult")
}
运行结果:
Java函数式API的使用
在Kotlin中调用Java的方法时,也可以使用函数式API,只不过有一定的条件限制。
具体来讲,如果一个Java方法只接收一个单抽象方法接口的参数,我们就可以在Kotlin中使用函数式API。单抽象方法接口指的是接口中只有一个待实现的抽象方法,如果有多个待实现的抽象方法,则无法使用函数式API。
比如 Java 中有一个最为常见的单抽象方法接口——Runnable接口,它内部只有一个 run() 抽象方法:
public interface Runnable {
public abstract void run();
}
那什么方法会只接收一个 Runnable 参数呢?
Thread类的构造方法就只接收一个Runnable参数,我们会这样写:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running");
}
});
在上述代码中,我们创建了一个Runnable接口的匿名类实例,并传入了 Thread 类的构造方法。如果代码转为 Kotlin 版本就是:
Thread(object : Runnable {
override fun run() {
println("Thread is running")
}
})
由于符合使用函数式API的条件,所以我们可以使用 Lambda 表达式对代码进行简化。
第一步是将 object 关键字去除,并将抽象方法也去除,只留实现的部分:
Thread(Runnable {
println("Thread is running")
})
因为Runnable类中只有一个抽象方法,即使我们没有显式地重写run()方法,Kotlin也能知道Runnable后面的Lambda表达式就是要在run()方法中实现的内容。
然后我们还可以将接口名进行省略,进一步简化:
Thread({
println("Thread is running")
})
当Lambda表达式是方法的最后一个参数时,可以将Lambda表达式移到括号外部。并且,如果Lambda表达式是唯一的参数,还可以将方法的括号省略,最终简化结果:
Thread {
println("Thread is running")
}
为什么我们要使用 Java 的函数式API?我们不能全都使用 Kotlin 的函数式API吗?
因为 Android SDK 是使用Java语言编写的,其中有大量的监听器和回调都是用 Java 定义的单抽象方法接口,而我们常常会在 Kotlin 中调用这些SDK接口,所以就会使用到。