Kotlin Lambda编程

许多现代高级编程语言在很早之前就开始支持Lambda编程了,但是Java却直到JDK 1.8之后才加入了Lambda编程的语法支持。

而Kotlin从第一个版本开始就支持了Lambda编程,并且Kotlin中的Lambda功能极为强大,甚至认为Lambda才是Kotlin的灵魂所在。

集合的创建与遍历

传统意义上的集合主要就是List和Set,再广泛一点的话,像Map这样的键值对数据结构也可以包含进来。

// Kotlin专门提供了一个内置的listOf()函数来简化初始化集合的写法
// listOf()函数创建的是一个不可变的集合
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
for (fruit in list) {
    println(fruit)
}
// 创建一个可变的集合呢?也很简单,使用mutableListOf()函数就可以了
val list2 = mutableListOf("Apple", "Banana", "Orange", "Pear", "Grape")
list2.add("Watermelon")
for (fruit in list2) {
    println(fruit)
}

// Set集合的用法几乎与此一模一样,只是将创建集合的方式换成了setOf()和mutableSetOf()函数
val set = setOf("Apple", "Banana", "Orange", "Pear", "Grape")
for (fruit in set) {
    println(fruit)
}

//Map是一种键值对形式的数据结构,因此在用法上和List、Set集合有较大的不同。
val map = HashMap<String, Int>()
map.put("Apple", 1)
map.put("Banana", 2)
map.put("Orange", 3)
// 在Kotlin中并不建议使用put()和get()方法来对Map进行添加和读取数据操作,而是更加推荐使用一种类似于数组下标的语法结构
val map2 = HashMap<String, Int>()
map2["Apple"] = 1
map2["Banana"] = 2
map2["Orange"] = 3
// 当然,这仍然不是最简便的写法,因为Kotlin毫无疑问地提供了一对mapOf()和mutableMapOf()函数来继续简化Map的用法。
// 在mapOf()函数中,我们可以直接传入初始化的键值对组合来完成对Map集合的创建:
val map3 = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)
//这里的键值对组合看上去好像是使用to这个关键字来进行关联的,但其实to并不是关键字,而是一个infix函数
for ((fruit, number) in map3) {
    println("fruit is " + fruit + ", number is " + number)
}

集合的函数式API

集合的函数式API有很多个,重点学习函数式API的语法结构,也就是Lambda表达式的语法结构。

Lambda的定义

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

Lambda表达式的语法结构

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

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

简化的写法

当然,在很多情况下,并不需要使用Lambda表达式完整的语法结构,而是有很多种简化的写法。

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
//这个Lambda参数是完全按照刚才学习的表达式的语法结构来定义的
val lambda = { fruit: String -> fruit.length }
val maxLengthFruit = list.maxBy(lambda)
//不需要专门定义一个lambda变量,而是可以直接将lambda表达式传入maxBy函数当中
val maxLengthFruit2 = list.maxBy({ fruit: String -> fruit.length })
//如果Lambda参数是函数的唯一一个参数的话,还可以将函数的括号省略:
val maxLengthFruit3 = list.maxBy { fruit: String -> fruit.length }
//由于Kotlin拥有出色的类型推导机制,Lambda表达式中的参数列表其实在大多数情况下不必声明参数类型
val maxLengthFruit4 = list.maxBy { fruit -> fruit.length }
//当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替
val maxLengthFruit5 = list.maxBy { it.length }
println("max length fruit is " + maxLengthFruit)

其实maxBy就是一个普通的函数而已,只不过它接收的是一个Lambda类型的参数,并且会在遍历集合时将每次遍历的值作为参数传递给Lambda表达式。maxBy函数的工作原理是根据我们传入的条件来遍历集合,从而找到该条件下的最大值

接下来就再来学习几个集合中比较常用的函数式API

map函数

集合中的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)
    }
}

filter函数

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

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

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

Java函数式API的使用

在Kotlin中调用Java方法时也可以使用函数式API,只不过这是有一定条件限制的。具体来讲,如果我们在Kotlin代码中调用了一个Java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。Java单抽象方法接口指的是接口中只有一个待实现方法,如果接口中有多个待实现方法,则无法使用函数式API。

Java原生API中有一个最为常见的单抽象方法接口——Runnable接口。这个接口中只有一个待实现的run()方法,定义如下:

public interface Runnable {
    void run();
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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 {
}

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

 

posted @ 2023-01-13 10:56  草木物语  阅读(43)  评论(0编辑  收藏  举报