Kotlin Lambda 核心用法

206 阅读7分钟

前言

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的特性:解构声明,也就是将两个变量放到一个括号中,这样每次遍历的结果会赋给这两个变量,键会赋值给第一个变量,值会赋值给第二个变量。

运行结果:

image.png

学习完集合的创建与遍历之后,就正式开始学习集合的函数式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)
    }
}

运行结果:

image.png

map函数的功能非常强大,它可以按照我们的需求对集合中的元素进行任意的映射转换,上面只是一个简单的示例而已。除此之外,还能完成更复杂的需求,只要在Lambda表示式中编写你需要的逻辑即可。

filter 函数

再来看看 filter 函数,它可用于过滤集合中的数据,比如我们可以将 filtermap 串联起来使用:

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 函数将筛选出的元素转为了大写。

运行结果:

image.png

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")
}

运行结果:

image.png

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接口,所以就会使用到。