Lambda编程
1 集合的创建与遍历
集合的函数式API是用来入门Lambda编程的绝佳示例,首先看看创建集合的方式。
传统意义上的集合主要就是List和Set,再广泛一点的话,像Map这样的键值对数据结构也可以包含进来。List、Set和Map在Java中都是接口,List的主要实现类是ArrayList和LinkedList,Set的主要实现类是HashSet,Map的主要实现类是HashMap,熟悉Java的人对这些集合的实现类一定不会陌生。
现在我们提出一个需求,创建一个包含许多水果名称的集合。如果是在Java中你会怎么实现?可能你首先会创建一个ArrayList的实例,然后将水果的名称一个个添加到集合中。当然,在Kotlin中也可以这么做:
val list = ArrayList<String>()
list.add("Apple")
list.add("Banana")
list.add("Orange")
list.add("Pear")
list.add("Grape")
但是这种初始化集合的方式比较烦琐,为此Kotlin专门提供了一个内置的listOf()函数来简化初始化集合的写法,如下所示:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
可以看到,这里仅用一行代码就完成了集合的初始化操作。
使用for-in循环不仅可以用来遍历区间,还可以用来遍历集合。现在我们就尝试一下使用for-in循环来遍历这个水果集合,在main()函数中编写如下代码:
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
for (fruit in list) {
println(fruit)
}
}
运行一下代码,结果如图所示。
不过需要注意的是,listOf()函数创建的是一个不可变的集合。不可变的集合指的就是该集合只能用于读取,我们无法对集合进行添加、修改或删除操作。
至于这么设计的理由,和val关键字、类默认不可继承的设计初衷是类似的,可见Kotlin在不可变性方面控制得极其严格。那如果我们确实需要创建一个可变的集合呢?也很简单,使用mutableListOf()函数就可以了,示例如下:
fun main() {
val list = mutableListOf("Apple", "Banana", "Orange", "Pear", "Grape")
list.add("Watermelon")
for (fruit in list) {
println(fruit)
}
}
这里先使用mutableListOf()函数创建一个可变的集合,然后向集合中添加了一个新的水果,最后再使用for-in循环对集合进行遍历。现在重新运行一下代码,结果如图所示。
可以看到,新添加到集合中的水果已经被成功打印出来了。
前面我们介绍的都是List集合的用法,实际上Set集合的用法几乎与此一模一样,只是将创建集合的方式换成了setOf()和mutableSetOf()函数而已。大致代码如下:
val set = setOf("Apple", "Banana", "Orange", "Pear", "Grape")
for (fruit in set) {
println(fruit)
}
需要注意,Set集合底层是使用hash映射机制来存放数据的,因此集合中的元素无法保证有序,这是和List集合最大的不同之处。
最后再来看一下Map集合的用法。Map是一种键值对形式的数据结构,因此在用法上和List、Set集合有较大的不同。传统的Map用法是先创建一个HashMap的实例,然后将一个个键值对数据添加到Map中。比如这里我们给每种水果设置一个对应的编号,就可以这样写:
val map = HashMap<String, Int>()
map.put("Apple", 1)
map.put("Banana", 2)
map.put("Orange", 3)
map.put("Pear", 4)
map.put("Grape", 5)
但其实在Kotlin中并不建议使用put()和get()方法来对Map进行添加和读取数据操作,而是更加推荐使用一种类似于数组下标的语法结构,比如向Map中添加一条数据就可以这么写:
map["Apple"] = 1
而从Map中读取一条数据就可以这么写:
val number = map["Apple"]
因此,上述代码经过优化过后就可以变成如下形式:
val map = HashMap<String, Int>()
map["Apple"] = 1
map["Banana"] = 2
map["Orange"] = 3
map["Pear"] = 4
map["Grape"] = 5
当然,这仍然不是最简便的写法,因为Kotlin毫无疑问地提供了一对mapOf()和mutableMapOf()函数来继续简化Map的用法。在mapOf()函数中,我们可以直接传入初始化的键值对组合来完成对Map集合的创建:
val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)
这里的键值对组合看上去好像是使用to这个关键字来进行关联的,但其实to并不是关键字,而是一个infix函数。
最后再来看一下如何遍历Map集合中的数据吧,其实使用的仍然是for-in循环。在main()函数中编写如下代码:
fun main() {
val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)
for ((fruit, number) in map) {
println("fruit is " + fruit + ", number is " + number)
}
}
这段代码主要的区别在于,在for-in循环中,我们将Map的键值对变量一起声明到了一对括号里面,这样当进行循环遍历时,每次遍历的结果就会赋值给这两个键值对变量,最后将它们的值打印出来。重新运行一下代码,结果如图所示。
2 集合的函数式API
集合的函数式API有很多个,这里我们重点了解函数式API的语法结构,也就是Lambda表达式的语法结构。
首先我们来思考一个需求,如何在一个水果集合里面找到单词最长的那个水果?当然这个需求很简单,也有很多种写法,你可能会很自然地写出如下代码:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
var maxLengthFruit = ""
for (fruit in list) {
if (fruit.length > maxLengthFruit.length) {
maxLengthFruit = fruit
}
}
println("max length fruit is " + maxLengthFruit)
这段代码很简洁,思路也很清晰,可以说是一段相当不错的代码了。但是如果我们使用集合的函数式API,就可以让这个功能变得更加容易:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val maxLengthFruit = list.maxBy { it.length }
println("max length fruit is " + maxLengthFruit)
上述代码使用的就是函数式API的用法,只用一行代码就能找到集合中单词最长的那个水果。
首先来看一下Lambda的定义,如果用最直白的语言来阐述的话,Lambda就是一小段可以作为参数传递的代码。
接着我们来看一下Lambda表达式的语法结构:
{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}
这是Lambda表达式最完整的语法结构定义。首先最外层是一对大括号,如果有参数传入到Lambda表达式中的话,我们还需要声明参数列表,参数列表的结尾使用一个->符号,表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码(虽然不建议编写太长的代码),并且最后一行代码会自动作为Lambda表达式的返回值。
当然,在很多情况下,我们并不需要使用Lambda表达式完整的语法结构,而是有很多种简化的写法。但是简化版的写法对于初学者而言更难理解,因此这里我准备使用一步步推导演化的方式,向你展示这些简化版的写法是从何而来的,这样你就能对Lambda表达式的语法结构理解得更加深刻了。那么接下来我们就由繁入简开始吧。
还是回到刚才找出最长单词水果的需求,前面使用的函数式API的语法结构看上去好像很特殊,但其实maxBy就是一个普通的函数而已,只不过它接收的是一个Lambda类型的参数,并且会在遍历集合时将每次遍历的值作为参数传递给Lambda表达式。maxBy函数的工作原理是根据我们传入的条件来遍历集合,从而找到该条件下的最大值,比如说想要找到单词最长的水果,那么条件自然就应该是单词的长度了。
理解了maxBy函数的工作原理之后,我们就可以开始套用刚才学习的Lambda表达式的语法结构,并将它传入到maxBy函数中了,如下所示:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val lambda = { fruit: String -> fruit.length }
val maxLengthFruit = list.maxBy(lambda)
可以看到,maxBy函数实质上就是接收了一个Lambda参数而已,并且这个Lambda参数是完全按照刚才学习的表达式的语法结构来定义的。
这种写法虽然可以正常工作,但是比较啰嗦,可简化的点也非常多,下面我们就开始对这段代码一步步进行简化。
首先,我们不需要专门定义一个lambda变量,而是可以直接将lambda表达式传入maxBy函数当中,因此第一步简化如下所示:
val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length })
然后Kotlin规定,当Lambda参数是函数的最后一个参数时,可以将Lambda表达式移到函数括号的外面,如下所示:
val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length }
接下来,如果Lambda参数是函数的唯一一个参数的话,还可以将函数的括号省略:
val maxLengthFruit = list.maxBy { fruit: String -> fruit.length }
由于Kotlin拥有出色的类型推导机制,Lambda表达式中的参数列表其实在大多数情况下不必声明参数类型,因此代码可以进一步简化成:
val maxLengthFruit = list.maxBy { fruit -> fruit.length }
最后,当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替,那么代码就变成了:
val maxLengthFruit = list.maxBy { it.length }
接下来我们就再来学习几个集合中比较常用的函数式API。
map
集合中的map函数是最常用的一种函数式API,它用于将集合中的每个元素都映射成一个另外的值,映射的规则在Lambda表达式中指定,最终生成一个新的集合。比如,这里我们希望让所有的水果名都变成大写模式,就可以这样写:
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val newList = list.map { it.toUpperCase() }
for (fruit in newList) {
println(fruit)
}
}
可以看到,我们在map函数的Lambda表示式中指定将单词转换成了大写模式,然后遍历这个新生成的集合。运行一下代码,结果如图所示。
map函数的功能非常强大,它可以按照我们的需求对集合中的元素进行任意的映射转换,上面只是一个简单的示例而已。除此之外,你还可以将水果名全部转换成小写,或者是只取单词的首字母,甚至是转换成单词长度这样一个数字集合,只要在Lambda表示式中编写你需要的逻辑即可。
filter
接下来我们再来学习另外一个比较常用的函数式API——filter函数。顾名思义,filter函数是用来过滤集合中的数据的,它可以单独使用,也可以配合刚才的map函数一起使用。
比如我们只想保留5个字母以内的水果,就可以借助filter函数来实现,代码如下所示:
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val newList = list.filter { it.length <= 5 }
.map { it.toUpperCase() }
for (fruit in newList) {
println(fruit)
}
}
可以看到,这里同时使用了filter和map函数,并通过Lambda表示式将水果单词长度限制在5个字母以内。重新运行一下代码,结果如图所示。
另外值得一提的是,上述代码中我们是先调用了filter函数再调用map函数。如果你改成先调用map函数再调用filter函数,也能实现同样的效果,但是效率就会差很多,因为这样相当于要对集合中所有的元素都进行一次映射转换后再进行过滤,这是完全不必要的。
any和all
接下来我们继续学习两个比较常用的函数式API——any和all函数。其中any函数用于判断集合中是否至少存在一个元素满足指定条件,all函数用于判断集合中是否所有元素都满足指定条件。由于这两个函数都很好理解,我们就直接通过代码示例学习了:
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val anyResult = list.any { it.length <= 5 }
val allResult = list.all { it.length <= 5 }
println("anyResult is " + anyResult + ", allResult is " + allResult)
}
这里还是在Lambda表达式中将条件设置为5个字母以内的单词,那么any函数就表示集合中是否存在5个字母以内的单词,而all函数就表示集合中是否所有单词都在5个字母以内。现在重新运行一下代码,结果如图所示。
3 Java函数式API的使用
现在我们已经学习了Kotlin中函数式API的用法,但实际上在Kotlin中调用Java方法时也可以使用函数式API,只不过这是有一定条件限制的。具体来讲,如果我们在Kotlin代码中调用了一个Java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。Java单抽象方法接口指的是接口中只有一个待实现方法,如果接口中有多个待实现方法,则无法使用函数式API。
Java原生API中有一个最为常见的单抽象方法接口——Runnable接口。这个接口中只有一个待实现的run()方法,定义如下:
public interface Runnable {
void run();
}
根据前面的讲解,对于任何一个Java方法,只要它接收Runnable参数,就可以使用函数式API。
Thread类的构造方法中接收了一个Runnable参数,我们可以使用如下Java代码创建并执行一个子线程:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running");
}
}).start();
注意,这里使用了匿名类的写法,我们创建了一个Runnable接口的匿名类实例,并将它传给了Thread类的构造方法,最后调用Thread类的start()方法执行这个线程。
而如果直接将这段代码翻译成Kotlin版本,写法将如下所示:
Thread(object : Runnable {
override fun run() {
println("Thread is running")
}
}).start()
Kotlin中匿名类的写法和Java有一点区别,由于Kotlin完全舍弃了new关键字,因此创建匿名类实例的时候就不能再使用new了,而是改用了object关键字。
目前Thread类的构造方法是符合Java函数式API的使用条件的,下面我们就看看如何对代码进行精简,如下所示:
Thread(Runnable {
println("Thread is running")
}).start()
这段代码明显简化了很多,既可以实现同样的功能,又不会造成任何歧义。因为Runnable类中只有一个待实现方法,即使这里没有显式地重写run()方法,Kotlin也能自动明白Runnable后面的Lambda表达式就是要在run()方法中实现的内容。
另外,如果一个Java方法的参数列表中不存在一个以上Java单抽象方法接口参数,我们还可以将接口名进行省略,这样代码就变得更加精简了:
Thread({
println("Thread is running")
}).start()
当Lambda表达式是方法的最后一个参数时,可以将Lambda表达式移到方法括号的外面。同时,如果Lambda表达式还是方法的唯一一个参数,还可以将方法的括号省略,最终简化结果如下:
Thread {
println("Thread is running")
}.start()
如果你将上述代码写到main()函数中并执行,就会得到如图2.29所示的结果。
举个例子,Android中有一个极为常用的点击事件接口OnClickListener,其定义如下:
public interface OnClickListener {
void onClick(View v);
}
可以看到,这又是一个单抽象方法接口。假设现在我们拥有一个按钮button的实例,然后使用Java代码去注册这个按钮的点击事件,需要这么写:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
而用Kotlin代码实现同样的功能,就可以使用函数式API的写法来对代码进行简化,结果如下:
button.setOnClickListener {
}
可以看到,使用这种写法,代码明显精简了很多。
ckListener {
void onClick(View v);
}
可以看到,这又是一个单抽象方法接口。假设现在我们拥有一个按钮button的实例,然后使用Java代码去注册这个按钮的点击事件,需要这么写:
```kotlin
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
而用Kotlin代码实现同样的功能,就可以使用函数式API的写法来对代码进行简化,结果如下:
button.setOnClickListener {
}
可以看到,使用这种写法,代码明显精简了很多。