Kotlin引入可空性的新特性,旨在消除来自代码空引用的危险。将运行时的NPE转变成编译器的错误。
在Kotlin类型系统中,分为可空类型和非空类型。当你允许一个变量为null时,需要显示在类型后面加上一个 问号 ,将其非空类型转换为可空类型。
常见的类型都是非空类型,不能存储null引用,只有在类型后面添加个问号转换为可空类型后,变量才可存储null引用。
val str:String? = null 复制代码
对于一个可空类型的值,不能直接调用该类型的方法,也不能把他赋值给非空类型,更不能把它传递给接受非空类型参数的函数。可空类型看似和非空类型并没有什么交互性,但其实并不是,只是需要对可空类型进行一个判空后,才能正常交互:
val str:String? = “” if (str != null) str.length 复制代码
一旦对可空类型的对象进行判空,编译器就会对判空的作用域内把该对象当作非空对待。
Java8中引入的特殊包装类型Optional来解决null引用问题。但这种方法使代码更加冗长,并且额外的包装类还影响运行时的性能,因此并没有被广泛使用起来。
但在Kotlin中,可空和非空的对象在运行时没有什么区别,可空类型并不是非空类型的包装类。所有的检查都是编译器完成,这使得Kotlin的可空类型并不会在运行时带来额外的开销。
Kotlin标准库中有一个高效的安全调度运算符:?. 。它将null检查和调用合并成一个操作。当你使用?.调用一个可空类型对象的方法时,若值不为空,则方法会被正常执行;若值为null,则方法调用不发生,并整个表达式返回null。
安全调用除了可以调用方法,还可以用来访问属性。
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) 复制代码
所以只有确保该可空类型对象不为空时,才使用非空断言。当使用非空断言而且发生异常时,异常栈只表明异常发生在哪一行,并不会指明哪个表达式,所以最好避免同一行中使用非空断言。
和常规的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函数将调用它的对象变成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" } } 复制代码
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){ } 复制代码