集合的创建与遍历
集合的函数式 API 是用来入门 Lambda 编程的绝佳示例,我们先来学习创建和遍历集合的方式。
List
Kotlin 提供内置的 listOf() 函数来简化初始化 list 集合的写法:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
遍历 list 集合:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
for (fruit in list) {
println(fruit)
}
不过需要注意的是:listOf() 函数创建的是一个不可变的集合。不可变的集合指的就是该集合只能用于读取,无法对集合进行添加、修改或者删除。
至于这么涉及到的理由,和 val 关键字、类默认不可继承的设计初衷是类似的,可见 Kotlin 在不可变性方面控制的很严格。
可以使用 mutableListOf() 函数创建可变的 list 集合:
val list = mutableListOf("Apple", "Banana", "Orange", "Pear", "Grape")
list.add("Watermelon")
for (fruit in list) {
println(fruit)
}
Set
和 List 集合的用法一样,只是将创建集合的方式换成了 setOf() 和 mutableSetOf() 函数。
需要注意,Set 集合底层使用 hash 映射机制来存放数据的,因此集合中的元素无法保证有序,这是和 List 集合最大的不同之处。
Map
创建 Map 有几种方式,除了调用 put() 和 get() 方法外,更推荐使用一种类似于数组下标的语法结构:
val map = HashMap<String, Int> ()
map["Apple"] = 1
map["Banana"] = 2
map["Orange"] = 3
map["Pear"] = 4
map["Grape"] = 5
Kotlin 还提供了一对 mapOf() 和 mutableMapOf() 函数来继续简化 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 循环:
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 的键值对变量一起声明到了一对括号里,这样当进行循环时,每次遍历的结果就会赋值给这两个键值对变量。
Lambda 表达式
Lambda 的定义,如果用最直白的语言来阐述的话,Lambda 就是一小段可以作为参数传递的代码。
接着我们看一下 Lambda 表达式的语法结构:
{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}
最外层是一对大括号,如果有参数传入到 Lambda 表达式的话,我们还需要声明参数列表,参数列表的结尾使用一个 -> 符号,表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码,并且最后一行代码会自动作为 Lambda 表达式的返回值。
很多情况下,我们并不需要使用 Lambda 表达式完整的语法结构,是有很多种简化的写法。下面我们将一步步推导演化,最终实现最简化的版本。
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val lambda = { fruit: String -> fruit.length}
val maxLengthFruit = list.maxByOrNull(lambda)
println(maxLengthFruit)
可以看到,maxByOrNull 函数实质上是接收了一个 Lambda 参数而已,并且这个 Lambda 参数是完全按照上面的完整的语法结构来定义的。
这种写法虽然可以正常工作,但是比较啰嗦,可简化的点也很多,下面开始一步步简化。
首先,不需要专门定义一个 lambda 变量,可以直接传入函数中:
val maxLengthFruit = list.maxByOrNull({ fruit: String -> fruit.length})
然后 Kotlin 规定,当 Lambda 参数是函数的最后一个参数时,可以将 Lambda 表达式移到函数括号的外面:
val maxLengthFruit = list.maxByOrNull() { fruit: String -> fruit.length}
解下来,如果 Lambda 参数是函数的唯一一个参数的话,还可以将函数的括号省略:
val maxLengthFruit = list.maxByOrNull { fruit: String -> fruit.length}
由于 Kotlin 有出色的类型推导机制,Lambda 表达式中的参数列表其实在大多数情况下不必声明参数类型:
val maxLengthFruit = list.maxByOrNull { fruit -> fruit.length}
最后,当 Lambda 表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用 it 关键字来代替:
val maxLengthFruit = list.maxByOrNull { it.length}
通过一步步推导的方式,得到最简洁的写法。
集合的函数式 API
集合的函数式 API 有很多个,上面展示了 maxByOrNull,下面介绍几个集合中比较常用的函数式 API。
map 函数
是最常用的一种函数式 API,它用于将集合中的每个元素映射成一个另外的之,映射的规则在 Lambda 表达式中指定:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val newList = list.map { it.uppercase() }
for (fruit in newList) {
println(fruit)
}
filter 函数
用于过滤集合中的数据,可以单独使用,也可以配合 map 函数一起使用:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val newList = list.filter { it.length <= 5 }.map { it.uppercase() }
for (fruit in newList) {
println(fruit)
}
注意 filter 和 map 函数的调用顺序不能调换,因为先过滤,之后再映射效率会高很多。
any 和 all 函数
any 函数用于判断集合中是否至少存在一个元素满足指定条件,all 函数用于判断集合中是否所有元素都满足指定条件。
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)
虽然集合中还有很多其他的函数时 API,但是只要掌握了基本的语法规则,其他的用法只要看看文档就能掌握了。
Java 函数式 API 的使用
现在我们已经学习了 Kotlin 中函数式 API 的用法,但实际上在 Kotlin 中调用 Java 方法也可以使用函数式 API,不过这是有一定条件限制的。如果在 Kotlin 代码中调用一个 Java 方法,并且该方法接收一个 Java 单抽象方法接口参数,就可以使用函数式 API。Java 单抽象方法接口指的是接口中只有一个抽象的方法,这种接口被称为函数式接口。
Java API 在 java.util.function 包中定义了很多非常通用的函数式接口。下表整理了 Java 中常用的函数式接口:
| 函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 |
|---|---|---|---|---|
| Runnable | 无 | void | run | 作为无参数或返回值的动作运行 |
| Supplier<T> | 无 | T | get | 提供一个 T 类型的值 |
| Consumer<T> | T | void | accept | 处理一个T类型的值 |
| Function<T,R> | T | R | apply | 有一个T类型参数的函数 |
| Predicate<T> | T | boolean | test | 布尔值函数 |
| UnaryOperator<T> | T | T | apply | 类型T上的一元操作符 |
| BinaryOperator<T> | T,T | T | apply | 类型T上的二元操作符 |
下面我们举两个例子,来看看 Kotlin 中 Java 的函数式 API 的使用。
以 Runnable 为例进行推导,先看看 Java 代码:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running");
}
}).start();
改成 Kotlin:
Thread(object : Runnable{
override fun run() {
println("Thread is running")
}
}).start()
Kotlin 中匿名类的写法和 Java 不同,由于舍弃了 new 关键字,因此创建匿名类实例的时候不能再使用 new 了,而是改用了 object 关键字。
因为 Runnable 类中只有一个待实现方法,即使没有显示的重写 run 方法,Kotlin 也能自动明白 Runnable 后面的 Lambda 表达式就是要运行 run 方法,所以对上述代码进行精简如下:
Thread(Runnable{
println("Thread is running")
}).start()
如果一个 Java 方法的参数列表中不存在一个以上 Java 单抽象方法接口参数,可以将接口名省略:
Thread({
println("Thread is running")
}).start()
到这里还没结束,和之前 Kotlin 中函数式 API 的用法类似,当 Lambda 表达式时方法的最后一个参数时,可以将 Lambda 表达式转移到方法括号的外面,同时如果 Lambda 表达式还是方法的唯一一个参数,还可以将方法的括号省略:
Thread{
println("Thread is running")
}.start()
除了 Runnable 类外,我们还看看 Android 中常用的点击事件接口 OnClickListener,这也是一个单抽象方法的接口:
public interface OnClickListener {
void onClick(View v)
}
Java 中点击事件代码如下:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(Viewv) {
}
});
而用 Kotlin 代码实现同样的功能,就可以使用函数式 API 的写法来对其进行简化:
button.setOnClickListener {
}
最后想再强调一下,Java 函数式 API 的使用都限定于从 Kotlin 中调用 Java 方法,并且但抽象方法接口也必须是使用 Java 语言定义的。为什么要这样设计呢?这是因为 Kotlin 中有专门的高阶函数来实现更加强大的自定义函数式 API 功能,从而不需要像 Java 这样借助单抽象方法接口来实现。