Kotlin lambda,有你想了解的一切-灵析社区

德州安卓

前言

在平时工作中,我们对于lambda表达式再熟系不过了,不过一直没有对lambda有个很明确的、系统的了解,那这篇文章就来仔细梳理一下。

正文

我们使用Kotlin时,比如给函数传递参数使用lambda,或者对集和处理时传入筛选条件也是lambda,可能你会认为这些代码再正常不过了,不过平时也没有仔细分析过,本篇文章就来系统介绍一下。

Java8为什么要引入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表达式简称为lambda,首先它是一个表达式,何为表达式,和语句最明显的区别就是表达式是有值的,而语句没有。

其次它就是一段代码,有固定的格式,这个很重要,格式如下:

注意这里的语法,必须由花括号给括起来,参数没有小括号,注意和函数类型给区分开。

Kotlin调用Java函数

前面我们看了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很像,但是是有区别的,后面再细说。

Java函数式接口

前面我们举例了lambda很好用,不过为什么Java定义的接口,kotlin或者Java 8可以使用lambda来替代匿名内部类的写法呢

当定义的接口只有一个抽象方法时,这种接口叫做函数时接口,或者叫做SAM接口,SAM代表抽象方法。比如Java中的Runnable、Callable接口等。

Kotlin允许在调用函数式接口作为参数时使用lambda,来保证代码的整洁。

注意,kotlin是拥有完全的函数类型,所以需要接送lambda作为参数的kotlin函数应该使用函数类型而不是函数式接口类型。所以kotlin不支持把lambda自动转换成实现kotlin接口的对象,所以用kotlin写的SAM接口,无法使用lambda。

函数式接口 VS 函数类型

这里很有意思,对于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捕捉

既然了解了捕捉的原理,那Kotlin的捕捉只不过把Java中的一些实现细节给优化了,比如捕捉val变量时,它的值会被拷贝下来,当捕捉var变量时,它的值会被作为Ref类的一个实例被保存下来。

传递lambda参数给Java的SAM类型参数的函数的原理

为什么要说这个呢 前面我们说了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构造方法

前面都是说的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是个接口,它肯定没有构造方法之说。

Java使用函数类型

其实前面我们说了函数类型和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接口的一个实现。

集合的函数式API

前面我们说了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

说到这里,我们来想一个问题,就是在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能写出简介代码的关键。

我们来简单回顾一些细节:

    • Java 8的lambda引入是为了解决Java单方法接口作为参数写匿名内部类的繁琐步骤。
    • 单方法的接口被叫做SAM接口或者函数式接口。
    • Kotlin调用Java的SAM接口参数传递lambda会被转换成对应的SAM接口的实例。
    • Kotlin可以直接使用SAM接口的构造函数来返回一个SAM接口类型的实例。
    • Java的匿名内部类或者lambda只能捕捉final变量,Kotlin都可以捕捉且修改。
    • Kotlin的高阶函数可以用lambda来表示,它是FunctionN接口的一个实现。
    • Kotlin拥有完整的函数类型,所以SAM接口在Kotlin中不用定义。
    • lambda在集和中使用非常多,为了减少lambda编译成匿名内部类情况造车的性能损耗,引入了内联函数的概念。
    • lambda就是一个代码块,默认其内部的this是指外面包括它的类,通过定义带接受者的lambda可以在lambda中访问其他对象的方法。

阅读量:1187

点赞量:0

收藏量:0