在平时工作中,我们对于lambda表达式再熟系不过了,不过一直没有对lambda有个很明确的、系统的了解,那这篇文章就来仔细梳理一下。
我们使用Kotlin时,比如给函数传递参数使用lambda,或者对集和处理时传入筛选条件也是lambda,可能你会认为这些代码再正常不过了,不过平时也没有仔细分析过,本篇文章就来系统介绍一下。
在我们使用Java 8之前,是没有lambda这个概念的,比如我们想写个按钮监听回调只能实现下面这种:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
这种写起来太麻烦了,当使用Java8时,就可以简化为下面代码:
button.setOnClickListener(v -> {
});
你或许觉得这种理所当然,但是Java8引入lambda却是很多人期望的。
其实Java8引入lambda就是为了解决这种单方法的接口,让写匿名内部类的繁琐代码给优化、简洁一些。
lambda表达式简称为lambda,首先它是一个表达式,何为表达式,和语句最明显的区别就是表达式是有值的,而语句没有。
其次它就是一段代码,有固定的格式,这个很重要,格式如下:
注意这里的语法,必须由花括号给括起来,参数没有小括号,注意和函数类型给区分开。
前面我们看了Java8对于单方法接口做的优化,但是我们很多代码都是Kotlin编写,而库的代码是Java的,所以Kotlin能调用上面代码很关键,比如下面例子:
//单方法的回调 使用Java定义一个单方法接口
interface SingleFunListener {
public int singleFunction(int k);
}
//接口的实例
private SingleFunListener singleFunctionListener;
//设置监听方法
public void setSingleFunctionListener(SingleFunListener singleFunctionListener){
this.singleFunctionListener = singleFunctionListener;
}
上面代码在Java中我们最熟悉了,现在我们来使用Kotlin调用上面代码:
//使用匿名内部类
val testJava = TestJava()
testJava.setSingleFunctionListener(object: SingleFunListener{
override fun singleFunction(k: Int): Int {
return 100
}
})
由于Kotlin可以使用高阶函数,可以把函数当做值来对待,所以我们可以定义一个函数,来当做参数:
//使用函数式编程
val listener:(Int) -> Int = {
100
}
testJava.setSingleFunctionListener(listener)
上面代码还可以进一步简化:
//使用lambda表达式
testJava.setSingleFunctionListener { k: Int ->
100
}
注意这里使用高阶函数和lambda很像,但是是有区别的,后面再细说。
前面我们举例了lambda很好用,不过为什么Java定义的接口,kotlin或者Java 8可以使用lambda来替代匿名内部类的写法呢
当定义的接口只有一个抽象方法时,这种接口叫做函数时接口,或者叫做SAM接口,SAM代表抽象方法。比如Java中的Runnable、Callable接口等。
Kotlin允许在调用函数式接口作为参数时使用lambda,来保证代码的整洁。
注意,kotlin是拥有完全的函数类型,所以需要接送lambda作为参数的kotlin函数应该使用函数类型而不是函数式接口类型。所以kotlin不支持把lambda自动转换成实现kotlin接口的对象,所以用kotlin写的SAM接口,无法使用lambda。
这里很有意思,对于SAM接口,Kotlin可以把lambda转换成对应的SAM接口实例,从而简化代码,这其实就是一个约定,但是当Kotlin代码定义SAM接口,将无法转换,因为Kotlin自己就有函数类型。
我们直接看个例子:
//KT文件单方法接口
interface OneMethodListener{
fun onMethod(a:Int)
}
//接口实例
private var listener:OneMethodListener? = null
//设置监听
fun setOnMethodListener(listener:OneMethodListener){
this.listener = listener
}
按理说,我这里调用setOnMethodListener时会提醒我们转换成lambda,我们看看:
IDE并没有提醒我们可以改成lambda,这就是不支持约定,因为在Kotlin中,这种SAM接口可以使用高阶函数来实现,比如下面代码:
//高阶函数
private var onMethodListener:((Int) -> Unit)? = null
fun setMethodListener(listener:((Int) -> Unit)?){
this.onMethodListener = listener
}
对于这种一样的功能的代码,我们调用时:
这里就可以把lambda传递给函数类型的变量,所以这个关键点要记住,使用Kotlin的话,对于SAM接口,就可以不用定义了,可以简化代码。
Kotlin为了让你这样做,也不让你直接把lambda转成Kotlin定义的SAM接口实例。
既然上面我们看到了定义的函数类型变量可以传递lambda,就必须要说一下啥是高阶函数。
高阶函数其实很简单,就是参数是其他函数或者返回值是其他函数的函数,那这里如何来传递一个函数呢 就是定义函数类型的变量。
比如上面的代码:
private var onMethodListener:((Int) -> Unit)? = null
这里的类型就是函数类型,但是这里最最关键的是Kotlin中,高阶函数可以用lambda来表示,这也是约定,所以期望传递的是函数类型的参数,可以用lambda来代替。
在Java中,我们在方法中使用匿名内部类或者lambda是无法直接使用方法的局部变量,比如下面代码:
这里不能访问和修改的原因很简单,这里lambda其实原理也就是匿名内部类,根据Java的生命周期我们知道引用类型对象的回收是通过GC来控制,但是方法的生命周期在当方法执行完便会介绍,所以如果在lambda表达式或者匿名内部类中调用方法的局部变量是不允许的。
但是在Kotlin中,Kotlin允许在lambda内部访问方法的非final变量甚至修改它们,比如下面代码:
//这里在lambda中访问tempString可以正常访问且修改
val testJava = TestJava()
var tempString = "zyh"
testJava.setSingleFunctionListener {
tempString = "wy"
100
}
看到这里是不是感觉很奇怪,Kotlin是如何做到的呢。
不管是Java还是Kotlin,如果从lambda中访问外部变量,我们就称为这些变量被lambda捕捉,比如上面的tempString变量。
为什么要捕捉呢 原因也就是变量的生命周期问题,如果变量被lambda捕捉了,使用这个变量的代码可以被存储并稍后再执行。其实也就是这个变量的保存地方改变了,函数生命周期正常运行。
当捕捉final变量时,它的值和使用这个值的lambda代码一起存储,而对于非final变量来说,它的值可以在lambda中进行修改,这时需要把它的值封装在一个特色的包容器中,这样就可以改变这个值,而对于包容器的引用会和lambda代码一起存储。
其实捕捉的原理非常容易,对于不可修改的变量,就直接把它提出来到和lambda一起保存,对于可修改的变量进行封装,这样引用的回收就是由GC处理,和匿名内部类一样了,就解决问题了。
既然了解了捕捉的原理,那Kotlin的捕捉只不过把Java中的一些实现细节给优化了,比如捕捉val变量时,它的值会被拷贝下来,当捕捉var变量时,它的值会被作为Ref类的一个实例被保存下来。
为什么要说这个呢 前面我们说了kotlin是有函数类型的,所以对于Java的SAM接口作为函数时,Kotlin可以传递给lambda给他,但是原理是啥,其实很简单Kotlin把lambda转换为了对应SAM接口的实例。
不过这里和一般的匿名内部类写法有点区别,它不是每次都创建一个实例,类似于在Java中创建一个匿名内部类的变量,然后反复使用这个变量。
//这里Kotlin调用Java SAM参数函数使用lambda
val testJava = TestJava()
testJava.setSingleFunctionListener {
100
}
上面代码等效于下面代码:
val singleListener : SingleFunListener = object: SingleFunListener{
override fun singleFunction(k: Int): Int {
return 100
}
}
testJava.setSingleFunctionListener(singleListener)
可以看出,当没有捕捉变量时,lambda可以少创建几个接口实例变量。
但是当捕捉了变量,这个就发生了变化,就变成了新实例,原因非常简单,它需要把捕捉的变量给放到自动生成的类中。
这里的原理由于是编译器做的,前面也说了Kotlin捕捉的原理,它类似如下:
//这里捕捉了tempString变量
val testJava = TestJava()
var tempString = "zyh"
testJava.setSingleFunctionListener {
tempString = "wy"
100
}
由于lambda捕捉的变量在不同函数中是不一样的,所以编译器自动生成的实例也不一样,上面类似于下面:
//这里捕捉了tempString
val singleListener : SingleFunListener$1 = object: SingleFunListener$1(var tempString: String){
override fun singleFunction(k: Int): Int {
tempString = "wy"
return 100
}
}
testJava.setSingleFunctionListener(singleListener)
如果在另一个函数中捕捉了其他变量,那这个生成的接口就会不一样,所以会有多个实例。
注意上面说的lambda转换成SAM接口,仅仅适用于kotlin调用Java的函数式方法时的处理,对于其他的,我们后面再说。
前面都是说的SAM接口作为函数参数,Kotlin为了方便可以传递lambda给他,但是当返回值是SAM接口时,这时需要通过lambda创建出一个SAM接口的实例,这时可以使用SAM构造方法。
这里直接看个例子:
//单方法的回调,这里返回Runnable类型的实例
interface SingleFunListener {
public Runnable singleFunction(int k);
}
//设置方法
public void setSingleFunctionListener(SingleFunListener singleFunctionListener){
this.singleFunctionListener = singleFunctionListener;
}
这里的代码我们使用匿名内部类很好解决,就创建一个类实现Runnable接口即可:
testJava.setSingleFunctionListener(object: SingleFunListener{
override fun singleFunction(k: Int): Runnable {
return Runnable { Logger.d("zyh") }
}
})
但是优雅的lambda怎么可能还会让你写这么多代码呢,可以简化为下面:
//直接把lambda传递给SAM接口
testJava.setSingleFunctionListener {
Runnable { Logger.d("zyh") }
}
这里也是一个约定了,Runnable是个接口,它肯定没有构造方法之说。
其实前面我们说了函数类型和lambda的区别,还说了Kotlin调用Java的SAM接口时会把lambda转成对于的SAM接口的实例,那对于Kotlin中定义的函数类型参数在Java中如何调用呢
//Kotlin代码定义的高阶函数
private var onMethodListener:((Int) -> Unit)? = null
fun setMethodListener(listener:((Int) -> Unit)?){
this.onMethodListener = listener
}
然后我们在Java代码中调用这个函数:
public void runTestJava(){
TestLambda testLambda = new TestLambda();
//调用函数类型参数的方法
testLambda.setMethodListener(new Function1<Integer, Unit>() {
@Override
public Unit invoke(Integer integer) {
return null;
}
});
}
会发现这里把高阶函数使用Function1这个接口实例来替换,其实到这里也就明了了,Kotlin的高阶函数其实就是FunctionN的接口的实现,每个接口都有一个invoke方法,调用invoke方法可以调用函数自己。
/** A function that takes 0 arguments. */
public interface Function0<out R> : Function<R> {
/** Invokes the function. */
public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
//....
/** A function that takes 22 arguments. */
public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R
}
会发现这里最多只能传入22个参数,哈哈,不过一般也不会传递这么多,这就是高阶函数的原理,其实也非常简单,对把你定义的高阶函数都变成FunctionN接口的一个实现。
前面我们说了Kotlin高阶函数的实现原理,那现在来说一个我们平时代码最常用的功能,就是集和的函数式API。
方法很多,这里就以filter为例:
//使用代码
val ints = arrayListOf(1, 2, 3, 4, 5)
val newInts = ints.filter { it > 2 }
//源码
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
我们查看源码会发现,首先它是一个扩展函数,然后参数predicate是函数类型,所以可以传递lambda,而且返回值必须是Boolean类型,这个由我们之前的知识都了解,注意这里会多个inline,来关注一下这个inline。
被inline修饰的函数叫做内联函数,上面的filter为什么要设置为内联呢
前面我们可知函数类型其实就是FunctionN接口的一个实例,所以filter传递的lambda会被编译成匿名内部类,看起来没啥问题,我们再看一下filter的实现:
//这里遍历集和,每个元素都调用一次函数类型参数
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
这里就有问题了,当集和有100个元素时,它会创建100个匿名内部类,这就有很严重的性能问题了,所以这里引入了内联函数的概念。
一句话说完,内联函数会把函数的实现给拷贝到调用的地方,这样就不会创建额外的匿名内部类实例了。
说到这里,我们来想一个问题,就是在lambda中使用this,这个this会指向什么东西 在编译器来看,lambda是一个代码块,所以this会指向是包围它的类,这里就介绍一个kotlin的lambda独特的功能,带接收者的lambda,在这个lambda中我们的this和it就指向了其他类实例。
比如代码:
val ints = arrayListOf(1, 2, 3, 4, 5)
ints.apply {
this.add(6)
}
这里就是apply和with函数,对于这2个函数我们再熟系不过了,不过为什么在这个lambda内部可以使用this呢,或许你没有注意过,我们看一下源码:
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
会发现这里定义的函数很普通,但是有一点区别,就是block的类型,它的类型是 T.() -> Unit,而不是普通的() -> Unit,这里就是Kotlin的独特地方,这里就定义了函数类型的参数它有了一个接受者,可以在lambda中使用this来访问这个接受者。
比如上面的apply源码中,类型参数T就是接受者,而且在lambda中可以访问该接受者的属性和方法。
其实lambda的知识还是蛮多的,很多细节都需要注意,他们是Kotlin能写出简介代码的关键。
我们来简单回顾一些细节:
阅读量:1187
点赞量:0
收藏量:0