前序
Kotlin引入可空性的新特性,旨在消除来自代码空引用的危险。将运行时的NPE转变成编译器的错误。
可空类型与非空类型
在Kotlin类型系统中,分为可空类型和非空类型。当你允许一个变量为null时,需要显示在类型后面加上一个问号,将其非空类型转换为可空类型。
常见的类型都是非空类型,不能存储null引用,只有在类型后面添加个问号转换为可空类型后,变量才可存储null引用。
val str:String? = null
对于一个可空类型的值,不能直接调用该类型的方法,也不能把他赋值给非空类型,更不能把它传递给接受非空类型参数的函数。可空类型看似和非空类型并没有什么交互性,但其实并不是,只是需要对可空类型进行一个判空后,才能正常交互:
val str:String? = “”
if (str != null)
str.length
一旦对可空类型的对象进行判空,编译器就会对判空的作用域内把该对象当作非空对待。
Kotlin的可空性与Java的Optional
Java8中引入的特殊包装类型Optional来解决null引用问题。但这种方法使代码更加冗长,并且额外的包装类还影响运行时的性能,因此并没有被广泛使用起来。
但在Kotlin中,可空和非空的对象在运行时没有什么区别,可空类型并不是非空类型的包装类。所有的检查都是编译器完成,这使得Kotlin的可空类型并不会在运行时带来额外的开销。
安全调用运算符:?.
Kotlin标准库中有一个高效的安全调度运算符:?. 。它将null检查和调用合并成一个操作。当你使用?.调用一个可空类型对象的方法时,若值不为空,则方法会被正常执行;若值为null,则方法调用不发生,并整个表达式返回null。
安全调用除了可以调用方法,还可以用来访问属性。
Elvis运算符:?:
Elvis运算符?:用来提供替代null的默认值。Elvis运算符接收两个表达式,如果左侧表达式非空,则返回其左侧表达式。当左侧表达式为空,则返回右侧表达式。
Elvis运算符经常与安全调度运算符一起使用:
val str:String? = null
println(str?.length ?: 0)
Elvis运算符也可以配合return 和 throw一起使用,当运算符左边为null时,能提前返回函数或抛出异常。
val str:String? = null
//为空抛一次
val length = str?.length ?: throw IllegalArgumentException()
println(length)
str?.let {
println(length)
} ?: return
//等价于
if(str == null)
//函数类型为空时直接打断函数继续执行
return
//str不为null,则继续执行。
println(length)
也可以配合run函数配合使用,替代if-lese:
str?.let {
//str不为空的逻辑
} ?: run {
//str为空时逻辑
}
非空断言:!!
Kotlin为NPE爱好者提供非空断言运算符 !! (双感叹号),可以把任何对象转换成非空类型,从而调用该对象方法,但可能造成抛出NPE。
val str:String? = null
//抛NPE
println(str!!.length)
所以只有确保该可空类型对象不为空时,才使用非空断言。当使用非空断言而且发生异常时,异常栈只表明异常发生在哪一行,并不会指明哪个表达式,所以最好避免同一行中使用非空断言。
安全转换:as?
和常规的Java转换一样,当被转换的值不是你视图转换的类型时,会抛出ClassCastException异常。一般解决方案是在使用在转换前使用is检查来确定该值是否符合转换类型。但Kotlin提供更简洁的运算符——安全转换运算符:as?
//定义父类和子类
open class Animal{
fun getName(){
}
}
class Dog:Animal(){
fun getDogName(){
}
}
fun main(args:Array<String>){
val animal:Animal = Dog()
val dog = animal as? Dog ?: return
dog.getDogName()
}
安全转换运算符尝试将值转换成给定的类型,否则返回null:
let函数
let函数将调用它的对象变成lambda表达式的参数。配合安全调度运算符可以把调用let函数的可空对象,转变成非空类型。然后在let函数中调用一系列对该可空类型的操作。
fun main(args:Array<String>){
val str:String? = null
str?.let {
daqi(it)
}
}
fun daqi(str:String){
}
当需要检查多个值是否为null时,不建议使用嵌套的let调用来处理,建议使用一个if语句对这些值进行一次性检查。
可空类型的扩展
对可空类型的进行扩展的好处是,允许接收者为null时调用扩展函数,并在扩展函数中处理null,而不用确保变量不为null后再调用该对象的方法。因为当实例为null时,成员方法永远不会被执行。
Kotlin标准库中的CharSequence存在两个扩展函数:isNullOrEmpty和isNullOrBlank,可以由String?类型的接收者调用。
对可空类型定义扩展函数时,意味着函数体中的this可能为空,需要做对应的空处理。
fun String?.daqi(){
if (this == null){
println("this is null")
}
}
fun main(args:Array<String>){
val str:String? = null
由于接收的是可空类型,不需要使用?.
str.daqi()
}
延迟初始化
Kotlin中,属性声明为非空类型时,必须在构造函数中初始化。但属性可以在一个特殊的方法中,通过依赖注入来初始化。这时不能在构造函数中为属性提供一个非空初始化器,但你仍想将该类型声明为非空类型,避免空检查。可以使用lateinit关键字修饰该变量,请将该变量使用var修饰,因为val必须会编译成必须在构造方法中初始化的final字段。
class daqi{
private lateinit var name:String
fun onCreate(){
name = "daqi"
}
}
可空性与Java
Kotlin会根据Java中的可空性注解,来对来自Java的类型分为可空类型和非空类型。如,@Nullable注解的对象,会被Kotlin当作可空类型的对象。@Notnull注解的对象,会被Kotlin当作非空类型的对象。
当可空性注解不存在时,Java类型会被转换为Kotlin的平台类型。平台类型本质上是Kotlin不知道其可空信息,既可以把它当作可空类型,也可以把它当作非空类型。如果选择非空类型,编译器会在赋值时触发一个断言,防止Kotlin的非空变量保存空值。这意味着需要开发者负责正确处理来自Java的值。
Kotlin定义的函数中,编译器会生成对每个非空类型的参数的检查,如果使用不正确的参数调用,会立即抛出异常。(这种检查在函数调用的时候就被执行了,而不是等到该异常参数被使用时才执行。)
基本数据类型
Java区分基本数据类型和引用类型,基本数据类型具有高效存储和传递的性质。当你需要在泛型类中存储一些基本数据类型时,需要以基本数据类型的包装类型进行存储。因为JVM不支持用基本数据类型作为类型参数。
Kotlin并不区分基本类型和包装类型。对于变量、属性和返回类型,Kotlin的基本数据类型会被编译成Java的基础数据类型。只有对于泛型类时,才会被编译器成对应的Java基本类型包装类。
基本数据类型
当使用Java声明的基本数据类型变量时,该类型会变成非空类型,而不是平台类型。因为Java的基本数据类型不能存储null值。
Kotlin中可空的基本数据类型会被编译成对应的包装类型,因为Java的基本数据类型不能存储null值。
数字转换
kotlin不会自动把数字从一种类型转换成另一种取值范围更大的类型。Kotlin为每种基本数据类型(Boolean除外)都定义了转换到其他基本数据类型的函数。
Kotlin要求转换必须显式的,因为在Java中,比较装箱值时,不仅检查他们存储的值,还会比较装箱类型。
//此处比较会返回false
new Integer(42).equals(new Long(42))
Kotlin标准库为字符串也提供了转换为基本数据类型的扩展函数。如果对字符串解析失败,则抛出NumberFormatException()方法。
根类型
Any类型是所有Kotlin非空类型的超类。但Any不能持有null值,当需要持有任何值的变量包括null值,必须使用Any?
Any只包含toString、equals和hashCode。所有Kotlin的这些方法都是从Any中继承来得。但Any不能使用使用其他Object的方法(如:wait和notify)
类型参数的可空性
Kotlin中所以泛型类和泛型函数的类型参数默认都是可空的,因为默认上界是Any?
如果需要类型参数非空,则必须为其指定一个非空的上界:
fun <T:Any> daqi(t:T){
}
参考资料:
- 《Kotlin实战》
- Kotlin官网