kotlin入门(六)lambda编程

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

而Kotlin从第一个版本开始就支持了Lambda编程,并且Kotlin中的Lambda功能极为强大,我甚至认为Lambda才是Kotlin的灵魂所在。不过,本章只是Kotlin的入门章节,我不可能在这短短一节里就将Lambda的方方面面全部覆盖。因此,这一节我们只学习一些Lambda编程的基础知识,而像高阶函数、DSL等高级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)
    }
}
复制代码

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

图1.png

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

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

fun main() {
    val list = mutableListOf("Apple1",
        "Banana1", "Orange1", "Pear1", "Grape1")
    list.add("Watermelon")
    for (fruit in list) {
        println(fruit)
    }
}
复制代码

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

图1.png

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

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

fun main() {
    val set = setOf("Apple123",
        "Banana", "Orange", "Pear", "Grape")
    for (fruit in set) {
        println(fruit)
    }
}
复制代码

需要注意,Set集合中是不可以存放重复元素的,如果存放了多个相同的元素,只会保留其中一份,这是和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)
复制代码

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

map["Apple"] = 1
复制代码

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

//    从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函数,我们会在本书第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的键值对变量一起声明到了一对括号里面,这样当进行循环遍历时,每次遍历的结果就会赋值给这两个键值对变量,最后将它们的值打印出来。重新运行一下代码,结果下图所示。

图1.png

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

2.集合的函数式API

集合的函数式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,就可以让这个功能变得更加容易:

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
    val maxLengthFruit = list.maxBy { it.length } //直接调用maxBy 函数
    println("max length fruit is " + maxLengthFruit)
}
复制代码

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

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

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

{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}
复制代码

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

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

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

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

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

    println("max length fruit is " + maxLengthFruit)
}
复制代码

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

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

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

fun main() {

    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
    val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length })

    println("max length fruit is " + maxLengthFruit)
}
复制代码

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

fun main() {

    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
    val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length }

    println("max length fruit is " + maxLengthFruit)
}
复制代码

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

fun main() {

    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
    val maxLengthFruit = list.maxBy { fruit: String -> fruit.length }
    
    println("max length fruit is " + maxLengthFruit)
}
复制代码

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

val maxLengthFruit = list.maxBy { fruit -> fruit.length }
复制代码

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

val maxLengthFruit = list.maxBy { it.length }
复制代码

怎么样?通过一步步推导的方式,我们就得到了和一开始那段函数式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表达式中指定将单词转换成了大写模式,然后遍历这个新 生成的集合。运行一下代码,结果下图所示。

图1.png

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

接下来我们再来学习另外一个比较常用的函数式API——filter函数。顾名思义,filter函数是用来过滤集合中的数据的,它可以单独使用,也可以配合刚才的map函数一起使用。比如我们只想保留5个字母以内的水果,就可以借助filter函数来实现,代码如下所示:

fun main() {
    val list = listOf("Apple", "Banana",
        "Orange", "Pear", "Grape", "Watermelon")
    val newList = list.map { it.toUpperCase() }
    for (fruit in newList) {
        print(fruit+"  ")
    }
}
复制代码

可以看到,这里同时使用了filter和map函数,并通过Lambda表示式将水果单词长度限制在 5个字母以内。重新运行一下代码,结果如下图所示。

图1.png

另外值得一提的是,上述代码中我们是先调用了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个字母以内。现在重新运行一下代码,结果如下图所示:

图1.png

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

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()
}
复制代码

运行一下后,结果如下图所示:

图1.png 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()
}
复制代码

运行一下后,结果如下图所示:

图1.png

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

举个例子,Android中有一个极为常用的点击事件接口OnClickListener,其定义如下:

//java写法
public interface OnClickListener {
    void onClick(View v);
}
复制代码

可以看到,这又是一个单抽象方法接口。假设现在我们拥有一个按钮button的实例,然后使用Java代码去注册这个按钮的点击事件,需要这么写:

//java写法

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
});
复制代码

而用Kotlin代码实现同样的功能,就可以使用函数式API的写法来对代码进行简化,结果如下:

//kotlin写法
button.setOnClickListener {
}
复制代码

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

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

猜你喜欢

转载自juejin.im/post/7074217732564058148