Android第一行代码——快速入门 Kotlin 编程(2.6 Lambda 编程)

本文介绍了Kotlin中的Lambda编程,包括集合的创建与遍历、函数式API的使用,以及如何在Kotlin中调用Java的函数式API。通过实例展示了Lambda表达式的简化写法和在集合操作中的应用,如`maxByOrNull`、`map`、`filter`等函数的使用,帮助读者快速入门Kotlin的Lambda编程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

2.6        Lambda 编程

2.6.1        集合的创建与遍历

2.6.2        集合的函数式 API

2.6.3        Java 函数式 API 的使用


2.6        Lambda 编程

        可能很多 Java 程序员对于 Lambda 编程还比较陌生,但其实这并不是什么新鲜的技术。许多现代高级编程语言在很早之前就开始支持 Lambda 编程了,但是 Java 却直到 JDK 1.8 之后才加入了 Lambda 编程的语法支持。因此,大量早期开发的 Java 和 Android 程序其实并未使用 Lambda 编程的特性。

        而 Kotlin 从第一个版本开始就支持了 Lambda 编程,并且 Kotlin 中的 Lambda 功能极为强大,我甚至认为 Lambda 才是 Kotlin 的灵魂所在。不过,本章只是 Kotlin 的入门章节,我不可能在这短短一节里就将 Lambda 的方方面面全部覆盖。因此,这一节我们只学习一些 Lambda 编程的基础知识,而像高阶函数、DSL 等高级 Lambda 技巧,我们会在本书的后续章节慢慢学习。

2.6.1        集合的创建与遍历

        集合的函数式 API 是用来入门 Lambda 编程的绝佳示例,不过在此之前,我们得先学习创建集合的方式才行。

        传统意义上的集合主要就是 List Set,再广泛一点的话,像 Map 这样的键值对数据结构也可以包含进来。ListSet Map 在 Java 中都是接口,List 的主要实现类是 ArrayList LinkedListSet 的主要实现类是 HashSetMap的主要实现类是 HashMap,熟悉 Java 的人对这些集合的实现类一定不会陌生。

        现在我们提出一个需求,创建一个包含许多水果名称的集合。如果是在 Java 中你会怎么实现? 可能你首先会创建一个 ArrayList 的实例,然后将水果的名称一个个添加到集合中。当然,在 Kotlin 中也可以这么做:

fun main(){
    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)
    }
}

        运行一下代码,结果如 图2.23 所示。

图2.23        对集合进行遍历

        不过需要注意的是,listOf() 函数创建的是一个不可变的集合。你也许不太能理解什么叫作不 可变的集合,因为在 Java 中这个概念不太常见。不可变的集合指的就是该集合只能用于读取, 我们无法对集合进行添加、修改或删除操作。

        至于这么设计的理由,和 val 关键字、类默认不可继承的设计初衷是类似的,可见 Kotlin 在不可变性方面控制得极其严格。那如果我们确实需要创建一个可变的集合呢?也很简单,使用 mutableListOf() 函数就可以了,示例如下:

fun main(){
    val list = mutableListOf("Apple","Banana","Orange","Pear","Grape")
    list.add("Watermelon")
    for (furit in list){
        println(furit)
    }
}

        这里先使用 mutableListOf() 函数创建一个可变的集合,然后向集合中添加了一个新的水 果,最后再使用 for-in 循环对集合进行遍历。现在重新运行一下代码,结果如 图2.24 所示。

图2.24        对可变集合进行遍历

        可以看到,新添加到集合中的水果已经被成功打印出来了。

        前面我们介绍的都是 List 集合的用法,实际上 Set 集合的用法几乎与此一模一样,只是将创建集合的方式换成了 setOf() mutableSetOf() 函数而已。大致代码如下:

fun main(){
    val set = setOf("Apple","Banana","Orange","Pear","Grape")
    for (furit in set){
        println(furit)
    }
}

        需要注意,Set 集合中是不可以存放重复元素的,如果存放了多个相同的元素,只会保留其中一份,这是和 List 集合最大的不同之处。当然这部分知识属于数据结构相关的内容,这里就不展 开讨论了。

        最后再来看一下 Map 集合的用法。Map 是一种键值对形式的数据结构,因此在用法上和ListSet 集合有较大的不同。传统的 Map 用法是先创建一个 HashMap 的实例,然后将一个个键值对数据添加到 Map 中。比如这里我们给每种水果设置一个对应的编号,就可以这样写:

fun main(){
    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)
}

        我之所以先用这种写法,是因为这种写法和 Java 语法是最相似的,因此可能最好理解。但其实在Kotlin中并不建议使用 put() get() 方法来对 Map 进行添加和读取数据操作,而是更加推荐 使用一种类似于数组下标的语法结构,比如向 Map 中添加一条数据就可以这么写:

 map["Apple"] = 1

        而从 Map 中读取一条数据就可以这么写:

val number = map["Apple"] 

        因此,上述代码经过优化过后就可以变成如下形式:

fun main(){
    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 函数,我们会在本书第 9 章的 Kotlin 课堂中深入探究 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.25 所示。 

图2.25        遍历 Map 中的数据

        好了,关于集合的创建与遍历就学到这里,接下来我们开始学习集合的函数式 API,从而正式入门 Lambda 编程。

小贴士:

        listOf() 函数 简化初始化集合的写法,创建的是一个不可变的集合,无法对集合进行添加、修改或删除操作。

        mutableListOf()函数 创建一个可变的集合。

        Set 集合 中是不可以存放重复元素的,如果存放了多个相同的元素,只会保留其中一份。

        mapOf()函数 ,我们可以直接传入初始化的键值对组合来完成对Map集合的创建

2.6.2        集合的函数式 API

        maxBy 函数 ,现在这个函数已经弃用,改为 maxByOrNull 函数 :

Deprecated: Use maxByOrNull instead.

        参考:maxBy - Kotlin Programming Language 


        集合的函数式 API 有很多个,这里我并不打算带你涉猎所有函数式 API 的用法,而是重点学习函数式 API 的语法结构,也就是 Lambda 表达式的语法结构。

        首先我们来思考一个需求,如何在一个水果集合里面找到单词最长的那个水果?当然这个需求很简单,也有很多种写法,你可能会很自然地写出如下代码:

fun main(){
    val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
    var maxLengthFilter = ""
    for (fruit in list) {
        if (fruit.length > maxLengthFilter.length){
            maxLengthFilter = fruit
        }
    }
    println("max length fruit is " + maxLengthFilter)
}

        这段代码很简洁,思路也很清晰,可以说是一段相当不错的代码了。但是如果我们使用集合的函数式API,就可以让这个功能变得更加容易:

fun main(){
    val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
    val maxLengthFilter = list.maxByOrNull {it.length}
    println("max length fruit is " + maxLengthFilter)
}

        上述代码使用的就是函数式 API 的用法,只用一行代码就能找到集合中单词最长的那个水果。或许你现在理解这段代码还比较吃力,那是因为我们还没有开始学习Lambda 表达式的语法结 构,等学完之后再来重新看这段代码时,你就会觉得非常简单易懂了。

        首先来看一下 Lambda 的定义,如果用最直白的语言来阐述的话,Lambda 就是一小段可以作为参数传递的代码。从定义上看,这个功能就很厉害了,因为正常情况下,我们向某个函数传 参时只能传入变量,而借助 Lambda 却允许传入一小段代码。这里两次使用了“一小段代码”这 种描述,那么到底多少代码才算一小段代码呢?Kotlin 对此并没有进行限制,但是通常不建议在 Lambda 表达式中编写太长的代码,否则可能会影响代码的可读性。

        接着我们来看一下Lambda 表达式的语法结构:

{ 参数名1: 参数类型,参数名2: 参数类型 -> 函数体 }

        这是 Lambda 表达式最完整的语法结构定义。首先最外层是一对大括号,如果有参数传入到 Lambda 表达式中的话,我们还需要声明参数列表,参数列表的结尾使用一个 -> 符号,表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码(虽然不建议编写太长的代码),并且最后一行代码会自动作为 Lambda 表达式的返回值。 

        当然,在很多情况下,我们并不需要使用 Lambda 表达式完整的语法结构,而是有很多种简化的写法。但是简化版的写法对于初学者而言更难理解,因此这里我准备使用一步步推导演化的 方式,向你展示这些简化版的写法是从何而来的,这样你就能对 Lambda 表达式的语法结构理解得更加深刻了。那么接下来我们就由繁入简开始吧。

        还是回到刚才找出最长单词水果的需求,前面使用的函数式 API 的语法结构看上去好像很特殊, 但其实 maxByOrNull 就是一个普通的函数而已,只不过它接收的是一个 Lambda 类型的参数,并且会在遍历集合时将每次遍历的值作为参数传递给 Lambda 表达式。maxByOrNull 函数的工作原理是根据我们传入的条件来遍历集合,从而找到该条件下的最大值,比如说想要找到单词最长的水果, 那么条件自然就应该是单词的长度了。

        理解了 maxByOrNull 函数的工作原理之后,我们就可以开始套用刚才学习的 Lambda 表达式的语法结 构,并将它传入到 maxByOrNull 函数中了,如下所示:

fun main(){
    val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
    val lambda = { fruit:String -> fruit.length }
    val maxLengthFilter = list.maxByOrNull(lambda)
    println(maxLengthFilter)
}

        可以看到,maxByOrNull 函数实质上就是接收了一个 Lambda 参数而已,并且这个 Lambda 参数是完全按照刚才学习的表达式的语法结构来定义的,因此这段代码应该算是比较好懂的。

        这种写法虽然可以正常工作,但是比较啰嗦,可简化的点也非常多,下面我们就开始对这段代 码一步步进行简化。

        首先,我们不需要专门定义一个 lambda 变量,而是可以直接将 lambda 表达式传入maxByOrNull 函数当中,因此第一步简化如下所示:

fun main(){
    val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
//    第一步:可以直接将lambda表达式传入maxBy函数当中
    val maxLengthFilter = list.maxByOrNull({ fruit:String -> fruit.length })
    println(maxLengthFilter)
}

        然后 Kotlin 规定,当 Lambda 参数是函数的最后一个参数时,可以将Lambda 表达式移到函数括号的外面,如下所示:

fun main(){
    val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
//    第一步:可以直接将lambda表达式传入maxBy函数当中
//    第二步:将Lambda 表达式移到函数括号的外面
    val maxLengthFilter = list.maxByOrNull(){ fruit:String -> fruit.length }
    println(maxLengthFilter)
}

        接下来,如果 Lambda 参数是函数的唯一一个参数的话,还可以将函数的括号省略:

fun main(){
    val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
//    第一步:可以直接将lambda表达式传入maxBy函数当中
//    第二步:将Lambda 表达式移到函数括号的外面
//    第三步:将函数的括号省略
    val maxLengthFilter = list.maxByOrNull{ fruit:String -> fruit.length }
    println(maxLengthFilter)
}

        这样代码看起来就变得清爽多了吧?但是我们还可以继续进行简化。由于 Kotlin 拥有出色的类型推导机制,Lambda 表达式中的参数列表其实在大多数情况下不必声明参数类型,因此代码可 以进一步简化成:

fun main(){
    val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
//    第一步:可以直接将lambda表达式传入maxBy函数当中
//    第二步:将Lambda 表达式移到函数括号的外面
//    第三步:将函数的括号省略
//    第四步:大多数情况不必声明参数类型
    val maxLengthFilter = list.maxByOrNull{ fruit -> fruit.length }
    println(maxLengthFilter)
}

        最后,当 Lambda 表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用 it 关键字来代替,那么代码就变成了:

fun main(){
    val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
//    第一步:可以直接将lambda表达式传入maxBy函数当中
//    第二步:将Lambda 表达式移到函数括号的外面
//    第三步:将函数的括号省略
//    第四步:大多数情况不必声明参数类型
//    第五步:使用 it 关键字代替
    val maxLengthFilter = list.maxByOrNull{ it.length }
    println(maxLengthFilter)
}

        怎么样?通过一步步推导的方式,我们就得到了和一开始那段函数式 API 一模一样的写法,是不是现在理解起来就非常轻松了呢?

        正如本小节开头所说的,这里我们重点学习的是函数式 API 的语法结构,理解了语法结构之后, 集合中的各种其他函数式 API 都是可以快速掌握的。

        接下来我们就再来学习几个集合中比较常用的函数式 API,相信这些对于现在的你来说,应该是没有什么困难的。

        集合中的 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 表达式中指定将单词转换成了大写模式,然后遍历这个新生成的集合。运行一下代码,结果如图2.26 所示。

图2.26        将水果名都转换成大写模式

        map 函数的功能非常强大,它可以按照我们的需求对集合中的元素进行任意的映射转换,上面只是一个简单的示例而已。除此之外,你还可以将水果名全部转换成小写,或者是只取单词的首 字母,甚至是转换成单词长度这样一个数字集合,只要在 Lambda 表示式中编写你需要的逻辑即可。

        接下来我们再来学习另外一个比较常用的函数式 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 个字母以内。重新运行一下代码,结果如 图2.27 所示。

图2.27        对水果单词长度进行过滤

        另外值得一提的是,上述代码中我们是先调用了 filter 函数再调用 map 函数。如果你改成先调用 map 函数再调用 filter 函数,也能实现同样的效果,但是效率就会差很多,因为这样相当于 要对集合中所有的元素都进行一次映射转换后再进行过滤,这是完全不必要的。而先进行过滤操作,再对过滤后的元素进行映射转换,就会明显高效得多。

        接下来我们继续学习两个比较常用的函数式 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 个字母以内。现在重新运行一下代码,结果如 图2.28 所示。

图2.28        any 和 all 函数的执行结果

        这样我们就将 Lambda 表达式的语法结构和几个常用的函数式 API 的用法都学习完了,虽然集合中还有许多其他函数式 API,但是只要掌握了基本的语法规则,其他函数式 API 的用法只要看一 看文档就能掌握了,相信这对你来说并不是难事。

小贴士:

maxByOrNull 函数 的工作原理是根据我们传入的条件来遍历集合,从而找到该条件下的最大值。

函数map 的功能非常强大,它可以按照我们的需求对集合中的元素进行任意的映射转换。

filter 函数 是用来过滤集合中的数据的,它可以单独使用,也可以配合 map 函数一起使用。

any 函数 用于判断集合中是否至少存在一个元素满足指定条件。

all 函数 用于判断集合中是否所有元素都满足指定条件。

2.6.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。那么什么 Java 方法接收了 Runnable 参数呢?这就有很多了,不过 Runnable 接口主要还 是结合线程来一起使用的,因此这里我们就通过 Java 的线程类 Thread 来学习一下。

        Thread 类的构造方法中接收了一个 Runnable 参数,我们可以使用如下 Java 代码创建并执行一个子线程:

new Thread(new Runnable(){
    @Override
    public void run {
        System.out.println("Thread is running");
    }
}).start();

        注意,这里使用了匿名类的写法,我们创建了一个 Runnable 接口的匿名类实例,并将它传给了 Thread 类的构造方法,最后调用 Thread 类的 start() 方法执行这个线程。 

        而如果直接将这段代码翻译成 Kotlin 版本,写法将如下所示:

fun main(){
    Thread(object : Runnable {
        override fun run() {
            println("Thread is running")
        }
    }).start()
}

        Kotlin 中匿名类的写法和 Java 有一点区别,由于 Kotlin 完全舍弃了 new 关键字,因此创建匿名类实例的时候就不能再使用 new 了,而是改用了object 关键字。这种写法虽然算不上复杂,但是相比于 Java 的匿名类写法,并没有什么简化之处。 

        但是别忘了,目前 Thread 类的构造方法是符合 Java 函数式 API 的使用条件的,下面我们就看看如何对代码进行精简,如下所示:

fun main(){
    Thread(Runnable{
        println("Thread is running")
    }).start()
}

        这段代码明显简化了很多,既可以实现同样的功能,又不会造成任何歧义。因为 Runnable 类中只有一个待实现方法,即使这里没有显式地重写 run() 方法,Kotlin也能自动明白Runnable 后 面的 Lambda 表达式就是要在 run() 方法中实现的内容。

        另外,如果一个Java 方法的参数列表中有且仅有一个 Java 单抽象方法接口参数,我们还可以将接口名进行省略,这样代码就变得更加精简了:

fun main(){
    Thread({
        println("Thread is running")
    }).start()
}

        不过到这里还没有结束,和之前 Kotlin 中函数式 API 的用法类似,当 Lambda 表达式是方法的最后一个参数时,可以将 Lambda 表达式移到方法括号的外面。同时,如果 Lambda 表达式还是方法的唯一一个参数,还可以将方法的括号省略,最终简化结果如下:

fun main(){
    Thread { 
        println("Thread is running")
    }.start()
}

        如果你将上述代码写到 main() 函数中并执行,就会得到如 图2.29 所示的结果。

图2.29        Java 函数式 API 的运行结果

        或许你会觉得,既然本书中所有的代码都是使用 Kotlin 编写的,这种 Java 函数式 API 应该并不常用吧?其实并不是这样的,因为我们后面要经常打交道的 Android SDK 还是使用 Java 语言编写的,当我们在 Kotlin 中调用这些 SDK 接口时,就很可能会用到这种 Java 函数式 API 的写法。

        举个例子,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 { 
} 

        可以看到,使用这种写法,代码明显精简了很多。这段给按钮注册点击事件的代码,我们在正式开始学习 Android 程序开发之后将会经常用到。

        最后提醒你一句,本小节中学习的 Java 函数式 API 的使用都限定于从 Kotlin 中调用 Java 方法,并且单抽象方法接口也必须是用 Java 语言定义的。你可能会好奇为什么要这样设计。这是因为 Kotlin 中有专门的高阶函数来实现更加强大的自定义函数式 API 功能,从而不需要像 Java 这样借助单抽象方法接口来实现。关于高阶函数的用法,我们会在本书的第6章进行学习。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值