半天入门了下 kotlin
最近好多人都在用 kotlin 了,我真实接触并且使用 kotlin 的时候是 2019 年。那个时候也依葫芦画瓢写了一篇《lateinit 和 by lazy 》的文章
但是,当时写代码还是停留在 if-else、 for 循环等等,跟 java 代码没什么区别,所以并没有深入的理解。
我目前是带着目的去看别人写的 kotlin 代码,我也不想一瞬间就能看懂别人写的 kotlin 代码,毕竟有很多诡异的写法,再加上每个人习惯不同,所以我只要求自己能看懂 80%,剩下的遇到的再去查。我也不要求自己能写的很好,目前只是能看懂,能改改,那就足够了。
超快从 Java -> Kotlin
首先 if-else、for 循环这些没什么区别,多了个 range,这个也不难理解,switch-case 变成了 when。业务代码大多是这种东西的堆叠,所以只要能看懂这个,就能看懂 kotlin 代码的 40%-50%。
其次,一些变量的修饰符,我觉得这个是我一开始很头疼的东西。
都说 var 是可变的, val 是不可变的,且均要进行初始化
即使赋值是 null,这个地方就关联了很多诡异的写法。
类成员变量初始化的各种写法
最基本的概念
var someObj: SomeObject = SomeObject(1)
someObj = SomeObject(2)
val finalObj: SomeObject = SomeObject(1)
finalObj = SomeObject(2) // 编译报错
基本概念就是 var 修饰的变量后续 可变
,val 不可变
。先这么记住,后续会存在 打脸
。
你会发现我这里声明的变量都初始化了,不初始化行不行?
// 注意这里的都是类成员变量,不是局部变量
var someObj: SomeObject // 报错
var someObj2: someObject = null // 报错
val finalObj: SomeObject // 报错
因为 kotlin 为了避免空指针,类成员变量不允许不初始化
(后续也会打脸),一定要初始化(你初始化为 null 也行,但是还会检测出你复制为 null 了,要求你转为 可空类型
)。
有可空类型,就有非空类型
-
可空类型
顾名思义,可能是空的类型。kotlin 在访问一个类的成员变量时,其实是访问的是其 get set 方法,他会去判空。可空类型在kotlin中的写法就是带个问号
var someObj2: someObject = null // 报错 var someObj3: someObject? = null // ok
-
非空类型
更简单了,不是空~~
打脸成员变量一定要初始化
看这段代码
lateinit var someObj: SomeObject
这里就是声明了一个叫做 someObj 的成员变量,类型是 SomeObject,声明变量时可以在冒号后指定类型,也可以不指定。这都不是重点。重点是,不是说一定要 初始化么
?好了,这里没有初始化,打脸了,但是看到一个关键字,lateinit
来了。
什么是 lateinit
?这是我 2019 年碰 kotlin 的时候,就了解了的。
想追原理,自己去看字节码。
总结一句话就是,我就是想打破 kotlin 的赋值初始化规则,让编译器不要管我有没有初始化了(为什么编译器要管初始化的问题?因为 kotlin 想尽力避免空指针的出现,所以都要初始化),我自己去负责这个变量的初始化。
那么问题来了,如果你用 lateinit
声明了一个变量,让编译器放你一马,之后你又没有去管理这个变量的初始化,在之后的代码里没有对它进行任何的赋值操作,那么你自己在运行时使用这个变量的时候,很显然啊,空指针啊!
那么问题又来了,既然 lateinit
只是说明我这个属性是暂时不初始化的,那我为什么不能直接用 var + null 声明呢?如下
var somObj: SomeObject = null
这样写对吗?铁定不对啊,编译器会给你报错 Null can not be a value of a non-null type SomeObject
,为啥?前面基本概念就说了,你这是可空类型,得加问号啊。但是如果你用 lateinit,就不需要加这个烦人的问号。
使用 var + null 初始化的可空类型的后续问题
一旦你的一个成员变量是可空类型,你在后续写代码的时候使用会有各种烦人的问题,比如下面这一坨代码。
private fun getLayoutView(): View? {
if (mLithoView == null) {
mLithoView = LithoView(rootView.context)
}
return mLithoView
}
var mPlcEntryDataAdapter: PlcEntryDataAdapter? = null
if (getLayoutView() != null) {
mStyleView = FlexStyleUtils.addStyleViewToParent(
rootView as RelativeLayout,
getLayoutView()!!,
getContainerId())
}
if (mPlcEntryDataAdapter?.actionType == PlcEntryStyleInfo.ActionType.APP_DOWNLOAD) {
mDownloadActionObserver.onNext(false)
}
你会发现有很多的 ?????
,居然还有 !!!!
。
是的没错,为了尽可能避免空指针,kotlin 要求所有成员变量都要初始化对吧,你居然还想初始化赋值为 null,那我 kotlin 辛辛苦苦想避免的空指针不就白费了么?所以,如果你一定要这么赋值,那么你就要在声明中加个问号,使用时也得带着问号,就是所谓的可空类型嘛,也就是上面代码里的 mPlcEntryDataAdapter?和 mPlcEntryDataAdapter?.actionType
这个变量的声明和使用的体现。
那!!
这个又是什么呢?
为什么他的 kotlin 代码里有 !!
!!
的作用其实就是告诉编译器,我这个可空类型的变量是非空的!(真打脸)
但是,这在 android 中是非常常见的。比如我们经常使用的 intent 来传递参数,可是 intent 是可空类型的,那么我们从 intent 里面读出来的参数,如果使用 var 声明,那么肯定也是可空类型的。但是,我们又知道,这个 intent 肯定不是空的,我们会传自己的参数进来,所以我们存在 intent 里面的参数也一定不是空的,我们接收参数的方法,也是接收一个非空参数。这个时候只能如下这样写用 !!
,或者 lateinit。
var config: AlbumDetectionConfig? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
config = intent?.let {
IntentUtils.getSerializableExtra<AlbumDetectionConfig>(
it,
AlbumDetectInitModuleALBUM_DETECTION_CONFIG_KEY
)}?:
emitter.onError(IllegalArgumentException("AlbumDetectionConfig must not be null!!"))
VideoInfo.buildVideoInfo(config!!, count++)
}
这里因为 intent 可空,所以 config 可空,又因为 buildVideoInfo 接收非空类型,所以我们只能用 !!
,或者 lateinit,但是 lateinit 只能用于声明全局变量,很多时候我们只需要一个局部变量,所以很多时候只能使用 var 和 !!
在上面的代码里,我们又会发现了两个诡异的东西 let
和 ?:
打脸 val 和 final
终于搞完了前面一大堆的 var 相关的类成员变量声明的问题,这时候我们一直谨记着 var 可变 val 不可变。
但是你又会看到这样的代码
val size: Int
get() {
return value?.size() ?: -1
}
首先看这个写法,咋还能这么写???是的,他就是可以这么写,可以重写 get 和 set 方法。
然后看 size 这个成员变量,这个成员变量是 val 声明的,但是···这不是随着 value 的值在变么?value 空了就是 -1,不是空就是 value 的大小。一直以为 val 声明的就是 final 的变量,其实不然
···
这其实是因为 kotlin 为我们生成了所有类成员变量的默认的 get set 方法,我们当然可以重写,我们声明的 val 只是声明了一个 final 的 get 方法仅此而已,而不是声明了一个 final 的成员变量,这个可以通过转 java 代码查看。
好了,至此,搞懂这些,应该能看懂 60%-70% 的 kotlin 代码了吧~~
烦人的各种 Scope Function
kotlin 比 java 多出了很多这些个东西,会阻断你的学习,让你觉得学起来很烦,不想学了。其实,到这里也就差不多了,知道了 let
和 ?:
,知道了 by or by lazy
,基本就差不多了。
当你查 let
的时候,你就会发现原来还有 with、run、apply、also
,让你想哭,但是这些都是功能不同罢了。
let
经常使用在上面代码中的场景下:可空类型不为空才会执行 let
的函数体,又因为 lambda 可以省略 return,所以上面的那一坨代码的意思是,intent 空的话那我就不执行 let 里面的函数体了,那就是个 null 了。?:
的意思就是,如果前面的是空则执行后面的代码。那么就是说,如果 intent 空了或者 intentUtils.getSerializableExtra() 的结果是空了,就执行 emitter.onError(),就是这么简单。
by
关键字是一块很多的东西,但是你看字节码或者转 java 代码看的时候就知道,这其实就是个委托的概念。
类和方法
这里 kotlin 也有很多新概念,你会发现你甚至没法继承类了,没法重写方法了。没错,因为一定要加 open
关键字
还有许许多多的概念,比如什么是伴生对象?什么是密封类?什么又是内联类?什么是幕后字段????????
这里只说几个常用的
伴生对象
首先 object
在 kotlin 中也是一个关键字,它可以用于
-
对象声明
懒加载的单例 -
伴生对象
也是我们经常看到的 companion object xxx 写法 -
匿名内部类
android 中也很常见 object: interface {}
而且可以随意增加方法,外部还能继续调用,而 java 中在匿名内部类声明的方法,外部是无法访问匿名内部类里新声明的属性或者方法的。
主要来看伴生对象,如果想快速理解的话就是伴生对象在 kotlin 中的感觉就像是 java 中的 static 一样,因为 kotlin 中没有 static 的概念。
companion object 是通过创建一个单例类来实现的。
如果你只是在 kotlin 代码中互相调用,是没有感知的,因为 kotlin 中 object 的声明,就代表了可以直接引用。
object A {
var a = "A"
fun a() {}
}
fun main(args: Array<String>) {
A.a // ok
A.a() // ok
}
这就跟 java 中的静态方法调用写法一致,但是实际 A 是个单例类,当你从 java 代码中调用时会明显发现不对劲。
public static void main(String[] args) {
A.a(); // error
A.INSTANCE.a(); // ok
}
这里就很明显了吧,其实就是一个单例类而已。而 companion object 同理,在 java 中调用时要用 A.Companion 来调用。
这里就有了另一个注解 @Jvmtatic,如果你加上了这个注解,那么编译器会在外部帮你生成一个真实的 java static 方法或者成员变量,那么你在 java 代码中调用的时候,就不需要 A.Companion 了,可以直接 A.a()!
数据类和密封类
数据类就是 data
关键字来声明,只是帮我们生成了一些常用的方法仅此而已。
密封类其实也没啥,是个抽象类,不过被严格要求了子类声明位置(所以密封了嘛···),这样可以对集成类型进行限制,算是一种代码设计层次的东西。
getter/setter 和 幕后字段
java 中我们要写 getter 和 setter,kotlin 中对于类型成员变量的方法和改变也是 getter 和 setter。
但是下面这段在 java 中看似没毛病的代码确是不行的~~
class A {
var a = ""
set(value) {
this.a = value
}
}
运行直接栈溢出
所以为了解决这个问题出现了 幕后字段
,用关键字 field
表示,我基本没用到过,所以刚才的代码要这么写
class A {
var a = ""
set(value) {
field = value
}
}
之后我看到了一些 幕后属性
的概念,但是没看到官方对此有任何的说明,举例也没有任何关键字,只是一种约定,感觉像是自己说出来的东西。
通过 _a,这种变量名写法表示是 幕后属性
?,然后再声明个 a,外部访问只能访问 a,不可访问 _a, a 的值依赖 _a,来达到一种可读不可写的作用域。
但是,直接声明个 a,private set,外部不就不可以调用了吗?内部依旧可以
协程
因为之前写过一段时间 go,go 也有 Goroutines,相类似的概念,但是 golang 可是自己写了一波调度器的,那 kotlin 如何实现的呢?
未完待续···
2021.1.10 补充
看了一波别人写的实现原理分析,源码也看了点,发现 kotlin 其实也是做了一套调度,帮忙更好的处理异步操作。
那既然如此,只能去慢慢学 api 了,没有办法的。
而且配上 kotlin 之后,加上 Android Architecture Components 那一套东西,帮你处理了很多生命周期和对象释放的问题等等···会让代码看起来十分简洁,不知道这算是对编程人员的要求更低了,还是要求更高了呢?
关于协程能否提升性能?这个看你怎么看 协程
这两个字了,如果你说的是 Goroutines,那确实可以提高性能,如果你说的是 kotlin 中的协程,不能,就是线程池而已。
为什么有这些区别?主要在于协程的实现上的区别,其实本质的纯洁的协程和性能没有半毛钱关系,它只是一段可供调用的逻辑段,谈何性能呢?
只不过,配合上线程,那么就能做到很多调度(也要看你怎么实现,但是这部分本身已经超出了最本质的协程的概念),写出同步形式的异步代码。