注册

Kotlin的一些细节与技巧

kotlin在使用过程中有些有着一些小细节需要注意,搞清楚这些能够更加高效的使用Kotlin,下面来介绍一下。


查看字节码


kotlin本质上在编译之后还是会跟java一样生成字节码,as的工具自带查看字节码工具,能让我们看到一些舒服的kotlin操作背后的秘密 image.png 点击生成文件的Decompile 能看到kotlin文件从字节码到java代码后的结果,不过可读性并不是很好 image.png


扩展方法的小坑


Kotlin提供了扩展方法和扩展属性,能够对一些我们无法修改源码的类,增加一些额外的方法和属性 一个很简单的例子,String是JDK提供的类,我们没有办法直接修改它的源码,但是我们又经常会做一些判空、判断长度的操作,在以往使用Java的时候,我们会使用TextUtils.isEmpty来判断,但是有了Kotlin之后,我们可以像下面这样,给String定义一个扩展方法,之后在方法体中,使用this就可以方法到当前的String对象,从而实现**「看起来」**为这个类新增了一个方法的效果,如下所示

fun main() {
    "".isEmpty()
}

fun String.isEmpty(): Boolean {
    return this.length > 0
}

这实际上是Kotlin编译器的魔法,最终它在调用时还是以一个方法的形式,「所以扩展方法并没有真正的为这个类增加新的方法」,而只是让你在写代码时可以像调用方法一样调用工具类,来增加代码的可读性,看下它的字节码 image.png 了解这一原理之后,我们就可以理解在一些特殊case下,Kotlin的扩展为什么表现的有点不符合预期,



  • 扩展类中一样签名的方法,将无效
class People {
    fun run() = println("people run")
}

fun People.run() = println("extend run")

fun main() {
    val people = People()

    people.run()
}
//people run

因为从底层来看,类People自己的方法和扩展方法,方法签名是一样的,Kotlin编译器发现本身有这个方法了,就不会再给你做扩展方法的调用



  • 扩展方法跟随声明时候类型
open class Fruit {
}

class Apple : Fruit() {

}


fun Fruit.printSelf() = println("Fruit")

fun Apple.printSelf() = println("Apple")

fun main() {
    val fruit = A()
    fruit.printSelf()
 //注意这里
    val apple1: Fruit = Apple()
    apple1.printSelf()

    val apple2 = Apple()
    apple2.printSelf()
}
// 输出结果是
// Fruit
// Fruit
// Apple

但是第二个的输出结果却是Fruit,把apple的类型声明成了Fruit,虽然它是一个Apple的实例,但Kotlin编译器又不知道你运行时到底是什么,你声明是Fruit,就给你调用Fruit的扩展方法。


inline来帮你性能优化


在高阶函数在调用时总会创建新的Function对象,当被频繁调用,那么就会有大量的对象被创建,除此之外还可能会有基础类型的装箱拆箱问题,不可避免的就会导致性能问题,为此,「Kotlin为我们提供了inline关键字」。 inline的作用**,内联**,通过inline,我们可以把**「函数调用替换到实际的调用处」**,从而避免Function对象的创建,进一步避免性能损耗,看下代码以及 image.png main方法的调用不再直接调用foo函数,而是把foo函数的函数体直接拷贝了过来进行调用, 不过也不能滥用inline,因为inline是在编译时进行代码的替换,那么就意味着你inline的函数体里的代码,会被替换到每一个调用的地方,从而导致字节码的膨胀,如果对产物对产物大小有严格的要求,需要关注下这个副作用。


借助reified来实现真泛型


在java中我们都知道由于编译时的类型擦除,JVM的泛型其实都是假泛型,如下的代码在编译时往往会报错

fun <T> foo() {
    println(T::class.java) // 会报错
}

但是Kotlin为我们提供了**「reified关键字」,通过这个关键字,我们就可以让上面的代码成功编译并且运行,不过还需要「搭配inline关键字」**

inline fun <reified T> fooReal() {
    println(T::class.java)
}

由于inline会把函数体替换到调用处,调用处的泛型类型一定是确定的,那么就可以直接把泛型参数进行替换,从而达成了「真泛型」的效果,比如使用上面的fooReal

fooReal<String>()
//调用它的打印方法时 替换为String类型
println(String::class.java)

Lateinit 和 by lazy的使用场景


这两个经常会被使用到用来实现变量的延迟初始化,不过二者还是有些区别的



  • lateinit

在声明变量时不知道它的初始值是多少,依赖后续的流程来赋值,可以节省变量判空带来的便利。不过需要确保后续是会对其赋值的,不然会有异常出现



  • lazy

**「一个对象的创建需要消耗大量的资源,而我不知道它到底会不会被用到」**的场景,并且只有在第一次被调用的时候才会去赋值。

fun main() {
    val lazyTest by lazy {
        println("create lazyTest instance")
    }

    println("before create")
    val value = lazyTest
    println("after create")
}
// before create
// create lazyTest instance
// after create

Sequence来提高性能


Kotlin为我们提供了大量的集合操作函数来简化对集合的操作,比如filter、map等,但是这些操作符往往**「伴随着性能的损耗」**,比如如下代码

fun main() {
    val list = (1..20).toList()

    val result = list.filter {
        print("$it ")
        it % 2 == 0
    }.also {
        println()
    }.filter {
        print("$it ")
        it % 5 == 0
    }
    println()
    println(result.toString())
}
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 
// 2 4 6 8 10 12 14 16 18 20 
// [10, 20]

可以看出,我们定义了一个1~20的集合,然后通过两次调用**「filter」**函数,来先筛选出集合中的偶数,再筛选出集合中的5的倍数,最后得到结果10和20,让我们看下这个舒服的fliter操作符的实现

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

可以看到,每次filter操作都会创建一个新的集合对象,如果你的操作次数很多并且你的集合对象很大,那么就会有额外的性能开销 「如果你对集合的操作次数比较多的话,这时候就需要Sequence来优化性能」

fun main() {
    val list = (1..20).toList()

    val sequenceResult = list.asSequence()
        .filter {
            print("$it ")
            it % 2 == 0
        }.filter {
            print("$it ")
            it % 5 == 0
        }
    
    val iterator = sequenceResult.iterator()
    iterator.forEach {
        print("result : $it ")
    }
}

// 1 2 2 3 4 4 5 6 6 7 8 8 9 10 10 result : 10 11 12 12 13 14 14 15 16 16 17 18 18 19 20 20 result : 20 

对于Sequence,由于它的计算是惰性的,在调用filter的时候,并不会立即计算,只有在调用它的iterator的next方法的时候才会进行计算,并且它并不会像List的filter一样计算完一个函数的结果之后才会去计算下一个函数的结果,「而是对于一个元素,用它直接去走完所有的计算」。 在上面的例子中,对于1,它走到第一个filter里面,不满足条件,直接就结束了,而对于5,它走到第一个filter里面,符合条件,这个时候会继续拿它去走第二个filter,不符合条件,就返回了,对于10,它走到第一个filter里面,符合条件,这个时候会继续拿它去走第二个filter,依然符合条件,最终就被输出了出来


Unit与void的区别


在Kotlin中,如果一个方法没有声明返回类型,那么它的返回类型会被默认设置为**「Unit」,但是「Unit并不等同于Java中的void」**关键字,void代表没有返回值,而Unit是有返回值的,如下

fun main() {
    val foo = foo()
    println(foo.javaClass)
}

fun foo() {

}

// 输出结果:class kotlin.Unit

继续跟进下看看Unit的实现

public object Unit {
    override fun toString() = "kotlin.Unit"
}

在Kotlin中是函数作为一等公民,而不是对象。这一个特性就决定了它可以使用函数进行传递和返回。因此,Kotlin中的高阶函数应用就很广。高阶函数至少就需要一个函数作为参数,或者返回一个函数。如果我们没有在明明函数声明中明确的指定返回类型,或者没有在Lambda函数中明确返回任何内容,它就会返回Unit。 比如 如下实现实际是相同的

fun funcionNoReturnAnything(){
 
}
fun funcionNoReturnAnything():Unit{
 
}

或者是在lambda函数体中最后一个值会作为返回值返回,如果没有明确返回,就会默认返回Unit

view.setOnclickListener{
 
}
view.setOnclickListener{
 Unit 
}

Kotlin的包装类型


kotlin是字节码层面跟java是一样的,但是java中在基础类型有着 **「原始类型和包装类型」**的区别,比如int和Integer,但是在kotlin中我们只有Int这一种类型,那么kotlin编译器是如何做到区分的呢?先看一段kotlin代码以及反编译java之后的代码 image.png 可以看出



  • 对于不可空的基础类型,Kotlin编译器会自动为我们选择使用原始类型,而对于可空的基础类型,Kotlin编译器则会选择使用包装类型
  • 对于集合这种只能传包装类的情况,不论你是传可空还是不可空,都会选择使用包装类型

老生常谈run、let、also、with


run、let、apply、also、with都是Kotlin官方为我们提供的高阶函数,通常对比着4个操作符,



  1. 差异

我们关注receiver、argument、return之间的差异,如图所示 image.png



  1. 场景

image.png 简而言之



  • **「run」**适用于在顶层进行初始化时使用
  • **「let」**在被可空对象调用时,适用于做null值的检查,let在被非空对象调用时,适用于做对象的映射计算,比如说从一个对象获取信息,之后对另一个对象进行初始化和设置最后返回新的对象
  • **「apply」**适用于做对象初始化之后的配置
  • **「also」**适用于与程序本身逻辑无关的副作用,比如说打印日志等

==和===


在Java中我们一般使用==来判断两个对象的引用是否相等,使用equals方法来判断两个**「对象值」**是否相等 但是在Kotlin中,==和equals是相等的用来判断值,使用===来判断两个对象的引用是否相等


高阶函数


kotlin中一等公民是函数,函数也可以作为另一个函数的入参或者返回值,这就是高阶函数。 不过JVM本身是没有函数类型的,那Kotlin是如何实现这种效果的呢?先看段kotlin代码以及反编译了java的代码,一切就一目了然 image.png 我们可以看到,最终foo方法传入的类型是一个Function0类型,然后调用了Function0的invoke方法,继续看下Function0类型

public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}

看来魔法就在这里 也就是说如下的两种写法也是等价的

//kotlin
fun main() {
    foo {
        println("foo")
    }
}
//java
public static void main(String[] args) {
    foo(new Function0<Unit>() {
        @Override
        public Unit invoke() {
            System.out.println("foo");
            return Unit.INSTANCE;
        }
    });
}

到这里是不是对高阶函数有着更深刻的认识了呢 Kotlin的高阶函数本质上是通过对函数的抽象,然后在运行时通过创建Function对象来实现的。


作者:Android茶话会
链接:https://juejin.cn/post/7250426696346878008
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册