Kotlin的一些细节与技巧

Kotlin的一些细节与技巧

欢迎重视「Android茶话会」更多精彩等你来探索

  1. 「学习之路」 取Android技术道路经典电子书
  2. 「天边好文」取天边论坛200+精彩博文,包括小说、形而上学等
  3. 「技术简历」取精选简历模板一份
  4. 「幼年游戏」取60+幼年游戏合集一份

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

查看字节码

kotlin本质上在编译之后仍是会跟java一样生成字节码,as的东西自带查看字节码东西,能让咱们看到一些舒畅的kotlin操作背后的秘密

Kotlin的一些细节与技巧
点击生成文件的Decompile 能看到kotlin文件从字节码到java代码后的成果,不过可读性并不是很好
Kotlin的一些细节与技巧

扩展办法的小坑

Kotlin供给了扩展办法和扩展特点,能够对一些咱们无法修正源码的类,添加一些额定的办法和特点 一个很简单的比方,String是JDK供给的类,咱们没有办法直接修正它的源码,可是咱们又经常会做一些判空、判别长度的操作,在以往运用Java的时分,咱们会运用TextUtils.isEmpty来判别,可是有了Kotlin之后,咱们能够像下面这样,给String定义一个扩展办法,之后在办法体中,运用this就能够办法到当时的String目标,然后完成**「看起来」**为这个类新增了一个办法的作用,如下所示

funmain(){
"".isEmpty()
}
funString.isEmpty():Boolean{
returnthis.length>0
}

这实践上是Kotlin编译器的魔法,终究它在调用时仍是以一个办法的方式,「所以扩展办法并没有真正的为这个类添加新的办法」,而只是让你在写代码时能够像调用办法一样调用东西类,来添加代码的可读性,看下它的字节码

Kotlin的一些细节与技巧
了解这一原理之后,咱们就能够理解在一些特别case下,Kotlin的扩展为什么表现的有点不符合预期,

  • 扩展类中一样签名的办法,将无效
classPeople{
funrun()=println("peoplerun")
}
funPeople.run()=println("extendrun")
funmain(){
valpeople=People()
people.run()
}
//peoplerun

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

  • 扩展办法跟从声明时分类型
openclassFruit{
}
classApple:Fruit(){
}
funFruit.printSelf()=println("Fruit")
funApple.printSelf()=println("Apple")
funmain(){
valfruit=A()
fruit.printSelf()
//留意这儿
valapple1:Fruit=Apple()
apple1.printSelf()
valapple2=Apple()
apple2.printSelf()
}
//输出成果是
//Fruit
//Fruit
//Apple

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

inline来帮你功能优化

在高阶函数在调用时总会创立新的Function目标,当被频频调用,那么就会有很多的目标被创立,除此之外还可能会有根底类型的装箱拆箱问题,不行防止的就会导致功能问题,为此,「Kotlin为咱们供给了inline关键字」。 inline的作用**,内联**,经过inline,咱们能够把**「函数调用替换到实践的调用途」**,然后防止Function目标的创立,进一步防止功能损耗,看下代码以及

Kotlin的一些细节与技巧
main办法的调用不再直接调用foo函数,而是把foo函数的函数体直接拷贝了过来进行调用, 不过也不能滥用inline,因为inline是在编译时进行代码的替换,那么就意味着你inline的函数体里的代码,会被替换到每一个调用的地方,然后导致字节码的膨胀,假如对产品对产品巨细有严厉的要求,需求重视下这个副作用。

借助reified来完成真泛型

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

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

可是Kotlin为咱们供给了**「reified关键字」,经过这个关键字,咱们就能够让上面的代码成功编译而且运行,不过还需求「搭配inline关键字」**

inlinefun<reifiedT>fooReal(){
println(T::class.java)
}

因为inline会把函数体替换到调用途,调用途的泛型类型一定是确认的,那么就能够直接把泛型参数进行替换,然后达成了「真泛型」的作用,比方运用上面的fooReal

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

Lateinit 和 by lazy的运用场景

这两个经常会被运用到用来完成变量的推迟初始化,不过二者仍是有些差异的

  • lateinit

在声明变量时不知道它的初始值是多少,依靠后续的流程来赋值,能够节省变量判空带来的便当。不过需求确保后续是会对其赋值的,否则会有异常出现

  • lazy

**「一个目标的创立需求耗费很多的资源,而我不知道它到底会不会被用到」**的场景,而且只要在第一次被调用的时分才会去赋值。

funmain(){
vallazyTestbylazy{
println("createlazyTestinstance")
}
println("beforecreate")
valvalue=lazyTest
println("aftercreate")
}
//beforecreate
//createlazyTestinstance
//aftercreate

Sequence来进步功能

Kotlin为咱们供给了很多的调集操作函数来简化对调集的操作,比方filter、map等,可是这些操作符往往**「伴随着功能的损耗」**,比方如下代码

funmain(){
vallist=(1..20).toList()
valresult=list.filter{
print("$it")
it%2==0
}.also{
println()
}.filter{
print("$it")
it%5==0
}
println()
println(result.toString())
}
//1234567891011121314151617181920
//2468101214161820
//[10,20]

能够看出,咱们定义了一个1~20的调集,然后经过两次调用**「filter」**函数,来先筛选出调集中的偶数,再筛选出调集中的5的倍数,终究得到成果10和20,让咱们看下这个舒畅的fliter操作符的完成

publicinlinefun<T>Iterable<T>.filter(predicate:(T)->Boolean):List<T>{
returnfilterTo(ArrayList<T>(),predicate)
}

能够看到,每次filter操作都会创立一个新的调集目标,假如你的操作次数很多而且你的调集目标很大,那么就会有额定的功能开支 「假如你对调集的操作次数比较多的话,这时分就需求Sequence来优化功能」

funmain(){
vallist=(1..20).toList()
valsequenceResult=list.asSequence()
.filter{
print("$it")
it%2==0
}.filter{
print("$it")
it%5==0
}

valiterator=sequenceResult.iterator()
iterator.forEach{
print("result:$it")
}
}
//12234456678891010result:10111212131414151616171818192020result:20

关于Sequence,因为它的核算是慵懒的,在调用filter的时分,并不会当即核算,只要在调用它的iterator的next办法的时分才会进行核算,而且它并不会像List的filter一样核算完一个函数的成果之后才会去核算下一个函数的成果,「而是关于一个元素,用它直接去走完一切的核算」。 在上面的比方中,关于1,它走到第一个filter里边,不满足条件,直接就结束了,而关于5,它走到第一个filter里边,符合条件,这个时分会持续拿它去走第二个filter,不符合条件,就回来了,关于10,它走到第一个filter里边,符合条件,这个时分会持续拿它去走第二个filter,依然符合条件,终究就被输出了出来

Unit与void的差异

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

funmain(){
valfoo=foo()
println(foo.javaClass)
}
funfoo(){
}
//输出成果:classkotlin.Unit

持续跟进下看看Unit的完成

publicobjectUnit{
overridefuntoString()="kotlin.Unit"
}

在Kotlin中是函数作为一等公民,而不是目标。这一个特性就决议了它能够运用函数进行传递和回来。因而,Kotlin中的高阶函数应用就很广。高阶函数至少就需求一个函数作为参数,或许回来一个函数。假如咱们没有在明明函数声明中清晰的指定回来类型,或许没有在Lambda函数中清晰回来任何内容,它就会回来Unit。 比方 如下完成实践是相同的

funfuncionNoReturnAnything(){

}
funfuncionNoReturnAnything():Unit{

}

或许是在lambda函数体中终究一个值会作为回来值回来,假如没有清晰回来,就会默许回来Unit

view.setOnclickListener{

}
view.setOnclickListener{
Unit
}

Kotlin的包装类型

kotlin是字节码层面跟java是一样的,可是java中在根底类型有着 **「原始类型和包装类型」**的差异,比方int和Integer,可是在kotlin中咱们只要Int这一种类型,那么kotlin编译器是怎么做到区分的呢?先看一段kotlin代码以及反编译java之后的代码

Kotlin的一些细节与技巧
能够看出

  • 关于不行空的根底类型,Kotlin编译器会自动为咱们选择运用原始类型,而关于可空的根底类型,Kotlin编译器则会选择运用包装类型
  • 关于调集这种只能传包装类的状况,不论你是传可空仍是不行空,都会选择运用包装类型

陈词滥调run、let、also、with

run、let、apply、also、with都是Kotlin官方为咱们供给的高阶函数,一般对比着4个操作符,

  1. 差异

咱们重视receiver、argument、return之间的差异,如图所示

Kotlin的一些细节与技巧

  1. 场景

Kotlin的一些细节与技巧
简而言之

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

==和===

在Java中咱们一般运用==来判别两个目标的引证是否相等,运用equals办法来判别两个**「目标值」**是否相等 可是在Kotlin中,==和equals是相等的用来判别值,运用===来判别两个目标的引证是否相等

高阶函数

kotlin中一等公民是函数,函数也能够作为另一个函数的入参或许回来值,这就是高阶函数。 不过JVM自身是没有函数类型的,那Kotlin是怎么完成这种作用的呢?先看段kotlin代码以及反编译了java的代码,一切就一目了然

Kotlin的一些细节与技巧
咱们能够看到,终究foo办法传入的类型是一个Function0类型,然后调用了Function0的invoke办法,持续看下Function0类型

publicinterfaceFunction0<outR>:Function<R>{
/**Invokesthefunction.*/
publicoperatorfuninvoke():R
}

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

//kotlin
funmain(){
foo{
println("foo")
}
}
//java
publicstaticvoidmain(String[]args){
foo(newFunction0<Unit>(){
@Override
publicUnitinvoke(){
System.out.println("foo");
returnUnit.INSTANCE;
}
});
}

到这儿是不是对高阶函数有着更深刻的认识了呢 Kotlin的高阶函数本质上是经过对函数的抽象,然后在运行时经过创立Function目标来完成的。

本期就到这儿

欢迎重视「Android茶话会」更多精彩等你来探索

  1. 「学习之路」 取Android技术道路经典电子书
  2. 「天边好文」取天边论坛200+精彩博文,包括小说、形而上学等
  3. 「技术简历」取精选简历模板一份
  4. 回**「幼年游戏」**取60+幼年游戏合集一份