【Kotlin】高阶函数和Lambda
文章目录
- 高阶函数
- 抽象和高阶函数
- 方法引用表达式使用场景
- Lambda表达式
- lambda表达式类型
- Lambda开销
- 闭包
- Java实现闭包
- Kotlin中的闭包
高阶函数
Kotlin天然支持了部分函数式特性。我们可以在一个函数内部定义一个局部函数。
fun foo(x: Int) {
fun double(y: Int): Int {
return y * 2
}
println(double(x))
}
抽象和高阶函数
在我们以往熟悉的编程中,过程限制为只能接收数据为参数
由于我们经常会遇到一些同样的程序设计模式能够用于不同的过程,比如一个包含了正整数的列表,需要对它的元素进行各种转换操作,例如对所有元素都乘以3,或者都除以2。我们就需要提供一种模式,同时接收这个列表及不同的元素操作过程,最终返回一个新的列表
为了把这种类似的模式描述为相应的概念,我们就需要构造出一种更加高级的过程,表现为:接收一个或多个过程为参数,或者以一个过程作为返回结果。这个就是所谓的高阶函数,你可以把它理解为“以其他函数作为参数或返回值的函数”。高阶函数
是一种更加高级的抽象机制,它极大地增强了语言的表达能力。
方法引用表达式使用场景
此外,我们还可以直接通过这种语法,来定义一个类的构造方法引用变量。
class Book(val name: String) {
fun main() {
val getBook = ::Book
println(getBook("Test").name)
}
}
可以发现,getBook类型为(name: String) -> Book。类似的道理,如果我们要引用某个类的成员变量,如Book类中的name,就可以这样引用:
Book::name
以上创建的Book::name的类型为(Book) -> String。当我们再对Book类对象的集合应用一些函数式API的时候,这会显得格外有用,比如:
fun main(args: Array<String>) {
val bookNames = listOf (
Book("Thinking in java")
Book("Dive into Kotlin")
).map(Book::name)
println(bookNames)
}
首先来看一下Lambda的定义,如果用最直白的语言来阐述的话,Lambda就是一小段可以作为参数传递的代码。从定义上看,这个功能就很厉害了,因为正常情况下,我们向某个函数传参时只能传入变量,而借助Lambda却允许传入一小段代码。这里两次使用了“一小段代码”这种描述,那么到底多少代码才算一小段代码呢?Kotlin对此并没有进行限制,但是通常不建议在Lambda表达式中编写太长的代码,否则可能会影响代码的可读性。
Lambda的语法:
- 一个Lambda表达式必须通过{}来包裹
- 如果Lambda声明了参数部分的类型,且返回值类型支持类型推导,那么Lambda变量就可以省略函数类型声明
- 如果Lambda变量声明了函数类型,那么Lambda的参数部分的类型就可以省略
此外,如果Lambda表达式返回的不是Unit,那么默认最后一行表达式的值类型就是返回值类型,如:
val foo = { x: Int ->
val y = x + 1
y // 返回值是y
}
Lambda表达式
{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}
“Lambda 表达式”(lambda expression)其实就是匿名函数,
Lambda
表达式基于数学中的λ
演算得名,直接对应于其中的lambda
抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda
表达式可以表示闭包。
Java 8
的一个大亮点是引入Lambda
表达式,使用它设计的代码会更加简洁。
// 没有使用Lambda的老方法:
button.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent ae){
System.out.println("Actiondetected");
}
});
// 使用Lambda:
button.addActionListener(()->{
System.out.println("Actiondetected");
});
// 不采用Lambda的老方法:
Runnable runnable1=new Runnable(){
@Override
public void run(){
System.out.println("RunningwithoutLambda");
}
};
// 使用Lambda:
Runnable runnable2=()->{
System.out.println("RunningfromLambda");
};
Lambda
能让代码更简洁,Kotlin的支持如下:
lambda
表达式总是被大括号括着- 其参数(如果有的话)在
->
之前声明(参数类型可以省略), - 函数体(如果存在的话)在
->
后面。
Lambda
表达式是定义匿名函数的简单方法。由于Lambda
表达式避免在抽象类或接口中编写明确的函数声明,进而也避免了类的实现部分,
所以它是非常有用的。
先看一个例子:
fun compare(a: String, b: String): Boolean {
return a.length < b.length
}
max(strings, compare)
就是找出strings
里面最长的那个。但是我个人觉得compare
还是很碍眼的,因为我并不想在后面引用他,那我怎么办呢,就是用“匿名函数”方式。
max(strings, (a,b)->{a.length < b.length})
(a,b)->{a.length < b.length}
就是一个没有名字的函数,直接作为参数赋给max
方法的第二个参数。但这个方法有很多东西都没有写明,如:
- 参数的类型
- 返回值的类型
但这些真的必要吗?a.length < b.length
很明显返回一个Boolean
的值,再就是max
的定义中肯定也定义了这个函数的参数类型和返回值类型。
这么明显的事为什么不让计算机自己去做而要让人写代码去做呢?这就是匿名函数的好处了。到这里,我们已经和Lambda
很接近了。
val sum: (Int, Int) -> Int = { x, y -> x + y }
Lambda
表达式就是被大括号括着的那一部分,在->
符号之前有参数声明,函数体跟在一个->
符号之后。
而且此Lambda
表达式之前有一个匿名的函数声明(在此例中两个Int
型的输入,一个Int
型的返回值),这个声明是可以不使用的。
则此Lambda
表达式变成val sum = { x: Int, y: Int -> x + y }
,此时Lambda
表达式会根据主体中的最后一个(或可能是单个)表达式会视为
返回值。当然,在某些特定情况下,x
、y
的类型了是可以推断的,所以val sum = { x, y -> x + y }
。
通过调用lambda来执行它的代码你可以使用invoke
函数调用lambda,并传入参数的值。例如,以下代码定义了变量addInts,并将用于将两个Int参数相加的lambda赋值给它。然后代码调用了该lambda,传入参数值6和7,并将结果赋值给变量result:
val addInts = { x: Int, y: Int -> x + y }
val result = addInts.invoke(6, 7)
// 还可以使用如下快捷方式调用lambda:
val result = addInts(6, 7)
lambda表达式类型
就像任何其他类型的对象一样,lambda也具有类型。然而,lambda类型的不同点在于,它不会为lambda的实现指定类名,而是指定lambda的参数和返回值的类型。lambda类型的格式如下:
(parameters) -> return_type
因此,如果你的lambda具有单独的Int参数并返回一个字符串,如下代码所示:
val msg = { x: Int -> "xxx" }
其类型为:
(Int) -> String
如果将lambda赋值给一个变量,编译器会根据该lambda来推测变量的类型,如上例所示。然而,就像任何其他类型的对象一样,你可以显式地定义该变量的类型。例如,以下代码定义了一个变量add,该变量可以保存对具有两个Int参数并返回Int类型的lambda的引用:
val add: (Int, Int) -> Int
add = { x: Int, y: Int -> x + y }
Lambda类型也被认为是函数类型。
当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it
关键字来代替.
val list = listOf("Apple", "Bnana", "Orange", "Pear")
val maxLengthFruit = list.maxBy {it.length}//按照长度排序
Lambda开销
fun foo(int: Int) = {
print(int)
}
listOf(1, 2, 3).forEach { foo(it) } // 对一个整数列表的元素遍历调用foo,我的评价是:高级
这里,你可定会纳闷it是啥?其实它也是Kotlin简化Lambda表达的一种语法糖,叫做单个参数的隐式名称,代表了这个Lambda所接收的单个参数。这里的调用等价于:
listOf(1, 2, 3).forEach { item -> foo(item) }
如果lambda具有一个单独的参数,而且编译器能够推断其类型,你可以省略该参数,并在lambda的主体中使用关键字it指代它。要了解它是如何工作的,如前所述,假设使用以下代码将lambda赋值给变量:
val addFive: (Int) -> Int = { x -> x + 5 }
由于lambda具有单独的参数x,而且编译器能够推断出x为Int类型,因此我们可以省略该x参数,并在lambda的主体中使用it替换它:
val addFive: (Int) -> Int = { it + 5 }
在上述代码中,{it+5}等价于{x->x+5},但更加简洁。请注意,你只能在编译器能够推断该参数类型的情况下使用it语法。例如,以下代码将无法编译,因为编译器不知道it应该是什么类型:
val addFive = { it + 5 } // 该代码无法编译,因为编译器不能推断其类型
我们看一下foo函数用IDE转换后的Java代码:
@JvmStatic
@NotNull
public static final Function0 foo(final int var0) {
return (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Ojbect invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
int var1 = var0;
System.out.printlln(var1);
}
});
}
以上是字节码反编译的Java代码,从中我们可以发现Kotlin实现Lambda表达式的机理。
闭包
在Kotlin中,你会发现匿名函数体、Lambda在语法上都存在“{}",由这对花括号包裹的代码如果访问了外部环境变量则被称为一个闭包。一个闭包可以被当做参数传递或直接使用,它可以简单的看成”访问外部环境变量的函数“。Lambda是Kotlin中最常见的闭包形式。
与Java不一样的地方在于,Kotlin中的闭包不仅可以访问外部变量,还能够对其进行修改(我有点疑惑,Java为啥不能修改?下面说),如下:
var sum = 0
listOf(1, 2, 3).filter { it > 0 }.forEach {
sum += it
}
println(sum) // 6
看到这里我是懵逼的? 到底什么是闭包? 闭包有什么作用?
闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。–百度百科
第一句总结的很简洁了:闭包就是能够读取其他函数内部变量的函数。
Java实现闭包
在Java8之前,是不支持闭包的,但是可以通过“接口+匿名内部类”来实现一个伪闭包的功能,为什么说是伪闭包?
简单的说就是: JVM在内部类初始化的时候帮我们拷贝了一个局部变量的备份到内部类中,并且把它的值复制到了堆内存中(变量有两份,同样的名字,一个在局部变量中用,一个在内部类中)。所以要是不用final修饰,那你后面把外部类中的变量的值修改了,而内部类中拷贝的值还是原来的,那这样岂不是两边的值不一样了? 所以不能让你改,必须加final。
Kotlin中的闭包
想要理解kotlin中闭包的实现,首先要懂kotlin中的一个概念:在Kotlin中,函数是“一等公民”。
对比一下java和kotlin更好理解:
在java中是不支持这种写法的,因为函数是“二等公民”。
下面再看下kotlin代码:
fun test(): () -> Unit {
var a = 0
return fun() {
a++
println(a)
}
}
fun main() {
val t = test()
t()
}
内部函数很轻松地调用了外部变量a。这只是一个最简单的闭包实现。按照这种思想,其他的实现例如:函数、条件语句、Lambda表达式等等都可以理解为闭包,这里不再赘述。不过万变不离其宗,只要记得一句话:闭包就是能够读取其他函数内部变量的函数。就是一个函数A可以访问另一个函数B的局部变量,即便另一个函数B执行完成了也没关系。目前把满足这样条件的函数A叫做闭包。