简述: 已经好久没有更新文章,这大概是2019年第二篇文章了,有不少小伙伴们都在公众号留言说是否是断更了、是否是跑路了。在这里统一回复下我还好,并无跑路哈,只是在思考接下来文章主要方向在哪? 如何在提高本身的同时能够帮助他人,以及这段时间也在不断认清本身和了解本身,发现本身哪里不足以及如何及时地查漏补缺。下面进入正题:java
Kotlin类型系统其中涉及到一个很重要的概念就是你们常说的可空性以及为何Kotlin相比Java在必定程度上能下降空指针异常。此外在Kotlin中彻底采用和Java不一样思路来定义它的类型系统。也正由于这样类型系统自然具备让Kotlin在空指针异常出现的频率明显低于Java出现的频率的优点。此外Kotlin考虑使用和Java彻底不一样类型系统,以及它是如何去作到极大兼容和互操做。android
在进入Kotlin类型系统以前,咱们不妨先一块儿来思考如下几个概念,若是不明确这几个概念很难从根本上去理解Kotlin类型系统,以及Kotlin在类型系统方面为何优于Java。算法
类型本质是什么呢? 为何变量拥有类型? 这两个问题在维基百科上给出了很好的回答. 类型实际上就是对数据的分类,决定了该类型上可能的值以及该类型的值上能够完成的操做。 须要特别去注意一下后面的阐述: "该类型上可能的值以及该类型的值上能够完成的操做。" 由于Java的类型系统其实并无100%符合这个规则,因此这也是Java类型系统所存在的问题,下面会作出具体的分析。设计模式
关于 类 和 类型估计不少开发者每每忽略它们之间的区别,由于在真正的应用场景并不会区分这么细。咱们在使用中每每会把类等同于类型,其实是彻底不一样两个东西。其实在Java中也有体现,例如List<String>、Lis<Integer>
和 List
,对于前者List<String>
和List<Integer>
只能是类型不能说是类, 而对于List
它既能够是List类也能够是类型(Java中的原生类型)。其实在Kotlin则把这个概念提高到一个更高的层次,由于Kotlin中每一个类多了一个可空类型,例如String
类就对应两种类型String
类型和String?
可空类型。而在Java中除了泛型类型,每一个类只对应一种类型(就是类的自己),因此每每被忽略。数组
咱们能够把Kotlin中的类可分为两大类(Java也能够这样划分): 泛型类和非泛型类安全
非泛型类数据结构
先说非泛型类也就是开发中接触最多的通常类,通常的类去定义一个变量的时候,它的类实际就是这个变量的类型。例如: var msg: String
这里咱们能够说String
类和msg
变量的类型是一致的。可是在Kotlin中还有一种特殊的类型那就是可空类型,能够定义为var msg: String?
,这里的String
类和msg
变量的String?
类型就不同了。因此在Kotlin中一个类通常至少对应两种类型. 因此类和类型不是一个东西。app
泛型类函数
泛型类比非泛型类要更加复杂,实际上一个泛型类能够对应无限种类型。为何这么说,其实很容易理解。咱们从前面文章知道,在定义泛型类的时候会定义泛型形参,要想拿到一个合法的泛型类型就须要在外部使用地方传入具体的类型实参替换定义中的类型形参。咱们知道在Kotlin中List
是一个类,它不是一个类型。由它能够衍生成无限种泛型类型例如List<String>、List<Int>、List<List<String>>、List<Map<String,Int>>
源码分析
咱们通常说子类就是派生类,该类通常会继承它的超类。例如: class Student: Person()
,这里的Student
通常称为Person
的子类, Person
是Student
的超类。
而子类型和超类型定义则彻底不同,咱们从上面类和类型区别就知道一个类能够有不少类型,那么子类型不只仅是想子类那样继承关系那么严格。 子类型定义的规则通常是这样的: 任什么时候候若是须要的是A类型值的任何地方,均可以使用B类型的值来替换的,那么就能够说B类型是A类型的子类型或者称A类型是B类型的超类型。能够明显看出子类型的规则会比子类规则更为宽松。那么咱们能够一块儿分析下面几个例子:
注意: 某个类型也是它本身自己的子类型,很明显Person类型的值任意出现地方,Person确定都是能够替换的。属于子类关系的通常也是子类型关系。像String类型值确定不能替代Int类型值出现的地方,因此它们不存在子类型关系
再来看个例子,全部类的非空类型都是该类对应的可空类型的子类型,可是反过来讲就不行,就好比Person
非空类型是Person?
可空类型的子类型,很明显嘛,任何Person?
可空类型出现值的地方,均可以使用Person
非空类型的值来替换。其实这些我在开发过程当中是能够体会获得的,好比细心的同窗就会发现,咱们在Kotlin开发过程,若是一个函数接收的是一个可空类型的参数,调用的地方传入一个非空类型的实参进去是合法的。可是若是一个函数接收的是非空类型参数,传入一个可空类型的实参编译器就会提示你,可能存在空指针问题,须要作非空判断。 由于咱们知道非空类型比可空类型更安全。来幅图理解下:
有了上述关于类型本质的阐述,咱们一块儿来看下Java中的一些基本类型来套用类型本质的定义,来看看有什么问题。
int
类型:例如一个int
类型的变量,那么代表它只能存储int
类型的数据,咱们都知道它用4个字节存储,数值表示范围是-2147483648 ~ 2147483647,那么规定该类型可能存在的值,而后咱们能够对该类型的值进行运算操做。彷佛没毛病,int
类型和类型本质阐述契合的是如此完美。可是String
类型呢?也是这样的吗?请接着往下看
String
类型或其余定义类对应的类型:例如一个String
类型的变量,在Java中它却能够存在两种值: 一个是String
类的实例另外一种则是null
。而后咱们能够对这些值进行一些操做,第一种String
类实例固然容许你调用String
类全部操做方法,可是对于第二种null
值,操做则很是有限,若是你强行使用null
值去操做String
类中的操做方法,那么恭喜你,你将得到一个NullPointerException
空指针异常。在Java中为了程序的健壮性,这就要求开发者对String
类型的值还得须要作额外的判断,而后再作相应的处理,若是不作额外判断处理那么就很容易获得空指针异常。 这就出现同一种类型变量存在多种值,却不能获得平等一致的对待。对比上述int
类型的存在的值都是一致对待,全部该类型上全部可能的值均可以进行相同的运算操做。下面接着看着一个颇有趣例子:
貌似连Java中的instanceof
都不认可null
是一个String
类型的值。这两种值的操做也彻底不同: 真实的String
容许你调用它的任何方法,而null
值只容许很是有限的操做。那么Kotlin类型系统是如何解决这样的问题的呢? 请接着往下看。
Java中的类型系统中String
类型或其余自定义类的类型,貌似和类型本质定义不太符合,该类型的全部可能值却被区别对待,存在二义性。还得额外判断,直接问题就是给开发者带来了额外负担得作非空判断,一旦处理很差就会出现空指针致使程序崩溃。这就是Java中引起空指针问题的本质。
抓住问题的本质,Kotlin作一个很伟大的举措那就是类型的拆分,将Kotlin中全部的类型拆分红两种: 一种是非空类型,另外一种则是可空类型;其中非空类型变量不容许null
值的赋值操做,换句话说就是String
非空类型只存在String
类的实例不存在null
值,因此针对String
非空类型的值你能够大胆使用String
类全部相关方法,不存在二义性。 固然也会存在null状况,那就可使用可空类型,在使用可空类型的变量的时候编译器在编译时期会作针对可空类型作必定判断,若是存在可空类型的变量操做该对应类的方法,就提示你须要作额外判空处理,这时候开发者就根据提示去作判空处理了,想象下都这样处理了,你的Kotlin代码还会出现空指针吗?(可是有一点很重要就是定义了一个变量你须要明确它是可空仍是非空,若是定义了可空类型你就须要对它负责,而且编译器也会提示帮助你对它作额外判空处理。)。一块儿来看下几个例子:
一、非空类型变量或常量不能接收null值
二、非空类型的变量或常量中is(至关于java中instanceof)
三、可空类型的变量或常量直接操做相应方法会有明显的编译错误并提示判空操做
然而上面那些都是Java给不了你的,因此Java程序中通常会存在三种状态: 一种佛系判空,常常会出现空指针问题。另外一种就是一股脑所有判空,但是代码中充斥着if-else
代码,可读性很是差。最后一种就是很是熟悉程序逻辑以及数据流向的开发者能够正常判断出哪里须要判空处理,哪里能够不须要,这一种对开发者要求极高,由于人老是会犯错的。
?.至关于判空处理,若是不为null就执行?.后面的表达式,不然就返回null
text?.substring(0,2) //至关于 if(text != null) text.substring(0,2) else null
复制代码
其实Kotlin为了类型判空处理可算是操碎了心,咱们都知道在Java中作判空处理无非就是if-else
或? xxx : xxx
三目运算符来实现。可是有时候出现嵌套判空的时候整个代码就是一个“箭头”,可读性就不好了。由以上例子可知?.
比if-else
省了不少代码,这还没法彻底显露它的优势,下面这个例子就更加明显了。
Java中的if-else 嵌套处理
Kotlin中的安全调用运算符?.链式调用处理
对比两种方式的实现你会不会以为Kotlin也许更适合你呢,利用?.链式调用的方式把嵌套if-else处理解开了。
若是?:前面表达式为null, 就执行?:后面的表达式,它通常会和?.一块儿使用。(注意: 它与Java中的? xxx : xxx 三目运算符不同) carbon (29).png
若是类型转化失败就返回null值,不然返回正确的类型转化后的值
val student = person as? Student//至关于 if(person is Student) person as Student else null
复制代码
非空断言运算符!!, 是强制告诉编译器这个变量的值不可能null,存在使用风险。一旦存在为null直接抛出空指针异常。
不少Kotlin开发者很厌恶这个操做符,以为写起来不优雅很影响代码的可读性,关于如何避免在Kotlin的代码中使用 !! 操做符。请参考我以前的一篇文章 [译]如何在你的Kotlin代码中移除全部的!!(非空断言).
实际上是非空断言的使用场景是存在的,例如你已经在一个函数中对某个变量进行判空处理了,可是后面逻辑中再次使用到了它而且你能够肯定它不可能为空,可能此时编译器没法识别它是不是非空,但因为它又是一个可空类型,那么它又会提示你进行判空处理,很烦人是不,不少人这时候可能就采用了 !! 确实缺少可读性。
针对上述问题,除了以前文章中给出解决方案,此次又提供一个新的解决方案,那就是契约(实际上主动告诉编译器某个规则,这样它就不会提示作判空处理了) 契约官方正式提出来是Kotlin1.3的版本,虽然还处于Experimental(好比自定义契约)中,可是实际上Kotlin内部代码,早就使用了契约。具体使用可参考我以前的一篇文章 JetBrains开发者日见闻(二)之Kotlin1.3的新特性(Contract契约与协程篇) 一块儿来看下内置契约是如何解决这个问题的。
经过上述咱们能够知道在Kotlin中拥有着与Java中彻底不同的类型系统。在Java中是不存在所谓的可空类型和非空类型。可是咱们都知道Kotlin与Java的互操性很强,几乎是彻底兼容Java。那么Kotlin是如何兼容Java中的变量类型的呢?咱们在Kotlin中确定须要常常调用Java代码,有的人可能会回答说Java中使用@NotNull和@Nullable
注解来标识。确实Kotlin能够识别多种不一样风格的注解,包括javax.annotation
、android.support.annotation
、org.jetbrains.annotation
等。可是一些以前的第三方库并无写的这么规范,显然没法经过这种方式彻底解决这个问题。
因此Kotlin引入一种新的概念叫作: 平台类型,平台类型本质上就是Kotlin不知道可空性信息的类型,既能够把它当作可空类型又能够把它当作非空类型。 这就意味你要像Java代码中同样对你在这个类型上作的操做负所有责任,说的有味道点就是你在Java中拉的便便,Kotlin是不会给你擦屁股的。因此对于Java中函数参数,Kotlin去调用的时候系统默认会处理可空类型(为了安全性考虑),若是你明确了不为空,能够直接把它修改成非空类型,系统也是不为报编译错误的,可是一旦这样处理了,你必须保证不能为空。
那么问题来了,不少人就疑问出于安全性考虑为何不直接所有转化可空类型呢? 实际上这种方案看似可行,实际上有点不妥,对于一些明确不可能为空的变量还须要作大量额外的判空操做就显得冗余。不然非空类型就没有存在的意义了。
咱们都知道在Java中针对基本数据类型和包装类型作了区分。例如一个基本数据类型int
的变量直接存储了它的值。而一个引用类型(包装类型) String
的变量仅仅存储的是指向该对象的内存地址的引用。基本数据类型有着自然的高效存储以及传递的优点,可是不能直接调用这些类型的方法,并且在Java中集合中不能将它做为泛型实参类型。
实际上在Kotlin中并无像Java那样分为了基本数据类型和包装类型,在Kotlin中永远是同一种类型。不少人估计会问了既然在Kotlin中基本数据类型和包装类型是同样的,那么是否是意味着Kotlin是使用引用类型来保存数据呢?是否是很是低效呢?不是这样的,Kotlin在运行时尽可能会把Int
等类型转换成Java中的int
基本数据类型,而遇到相似集合或泛型的时候就会转化成Java中对应的Integer
等包装类型。这其实是一个底层优化,至于什么场景转化成int
,什么场景转化成Integer
,关于这块能够参考以前一篇有关内联类自动装箱和拆箱的文章: [译]Kotlin中内联类的自动装箱和高性能探索(二)
基本数据类型也分为可空类型和非空类型, 具体可参考以下的类型层次结构图:
Any类型是全部非空类型的超类型,Any?类型则是全部的类型的超类型,便是非空类型的超类型也是全部可空类型的超类型。由于Any?是Any的超类型。具体的层次可参考下面这张图:
Unit类型也便是Kotlin中的空类型,至关于Java中的void类型,默认状况下它能够被省略
Nothing类型是全部类型的子类型,它既是全部非空类型的子类型也是全部可空类型的子类型,由于Nothing是Nothing?的子类型,然而Nothing?又是全部可空类型的子类型。 具体能够看下以下的层次结构图:
在Collection只具备访问元素的方法,不具备相似add、remove、clear之类的方法,而在MutableCollection中则相比Collection多出了修改元素的方法。
Collection只读集合与MutableCollectio可变集合联系:
MutableCollection其实是Collection集合接口的子接口,他们之间是继承关系。
经过Collection.kt文件中能够了解到有这些集合Iterable(只读迭代器)和MutableIterable(可变迭代器)、Collection和MutableCollection、List和MutableList、Set和MutableSet、Map和MutableMap。那么它们之间的类关系图是怎样的。
Iterable和MutableIterable接口分别是只读和可变集合的父接口,Collection继承Iterable而后List、Set接口继承自Collection,Map接口比较特殊它是单独的接口,而后MutableMap接口是继承自Map.
咱们刚刚说到在Kotlin中集合的设计与Java不同,可是每个Kotlin的接口都是其对应的Java集合接口的一个实例,也就是在Kotlin中集合与Kotlin中的集合存在必定的对应关系。Java中的ArrayList类和HashSet类实际上Kotlin中的MutableList和MutableSet集合接口的实现类。把这种关系加上,上面的类关系图能够进一步完善。
因为在Kotlin中集合主要分为了只读集合和可变集合,那么初始化只读集合和可变集合的函数也不同。以List集合为例,对于只读集合初始化通常采用listOf()方法,对于可变集合初始化通常采用mutableListOf()或者直接建立ArrayList<E>,由于mutableListOf()内部实现也是也仍是采用建立ArrayList,这个ArrayList其实是Java中的java.util.ArrayList<E>,只不过在Kotlin中使用typealias(关于typealias的使用以前博客有过详细介绍)取了别名而已。关于具体内容请参考这个类kotlin.collections.TypeAliasesKt实现
注意点一: 在代码的任何地方都优先使用只读集合,只在须要修改集合的状况下才去使用可变集合
注意点二: 只读集合不必定是不可变的,关于这个只读和不可变相似于val的只读和不可变原理。
注意点三: 不能把一个只读类型的集合做为参数传递给一个带可变类型集合的函数。
正如前面所说起的可空性平台类型同样,Kotlin中没法知道可空性信息的类型,既能够把它当作可空类型又能够把它当作非空类型。集合的平台类型和这个相似,在Java中声明的集合类型的变量也被看作平台类型。一个平台类型的集合本质上就是可变性未知的集合,Kotlin中能够把它看作是只读的集合或者是可变的集合. 实际上这都不是很重要,由于你只须要根据你的需求选择便可,想要执行的全部操做都能正常工做,它不像可空性平台存在额外判断操做以及空指针风险。
注意: 但是当你决定使用哪种Kotlin类型表示Java中集合类型的变量时,须要考虑如下三种状况:
若是为空转换成Kotlin中集合后面添加 ?,例如Java中的
List<String>
转化成Kotlin中的List<String>?
若是为空转换成Kotlin中集合泛型实参后面添加 ?,例如Java中的
List<String>
转化成Kotlin中的List<String?>
若是是只读的,例如Java中的
List<String>
转化成Kotlin中的List<String>
;若是是可变的,例如Java中的List<String>
转化成Kotlin中的MutableList<String>
.
注意: 固然上面三种状况能够一种或多种同时出现,那么转化成Kotlin中的集合类型也是多种状况最终重组的类型。
到这里有关Kotlin的类型系统基本就说得差很少,该涉及到的内容基本都涉及了。其实仔细去体会下为何Kotlin的类型系统要如此设计,确实是它必定道理的。咱们常常听别人夸Kotlin比Java优势是啥,不少人都说少了不少空指针异常,可是为何能Kotlin相比Java有更少的空指针异常相信这篇文章也足够回答你了吧。
接下来再扯点别的你们都知道Android开发已经进入了一个平稳期了, 泡沫逐渐散去, 那么对Android开发者的要求也会愈来愈高,只会使用的API时代早已通过去了,因此开发者须要不断调整本身不断提高本身的能力来面对这些变化。分析过源码的小伙伴就知道看懂源码其中最关键点就是源码中使用的数据结构算法以及使用一些高级的设计模式。正由于这样后期文章方向会针对数据结构算法、设计模式、源码分析这块作必定输出,近期计划是每周一篇Kotlin相关文章(原创或翻译),每周一篇设计模式相关和每周一篇数据结构算法相关(结合LeetCode上的题目)。
欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~
原创系列:
Effective Kotlin翻译系列
翻译系列:
实战系列: