Kotlin 学习之 Lambda 编程

287 阅读6分钟

集合的创建与遍历

集合的函数式 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 中常用的函数式接口:

函数式接口参数类型返回类型抽象方法名描述
Runnablevoidrun作为无参数或返回值的动作运行
Supplier<T>Tget提供一个 T 类型的值
Consumer<T>Tvoidaccept处理一个T类型的值
Function<T,R>TRapply有一个T类型参数的函数
Predicate<T>Tbooleantest布尔值函数
UnaryOperator<T>TTapply类型T上的一元操作符
BinaryOperator<T>T,TTapply类型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 这样借助单抽象方法接口来实现。