- 许多编程语言(包括Java)中最常见的异常之一就是NullPointException(NPE),即访问空引用的成员会导致空引用异常
- Kotlin在代码中消除了NullPointException,区分了一个引用为非空类型(non-nullable)和可空类型(nullable)
一、非空类型:non-nullable
1、变量默认是non-nullable
类型的
2、non-nullable
变量不能赋值为空,否者会编译报错
3、调用非空类型变量的属性或者方法,保证不会导致NPE
,可以放心使用
var a = "danke"
a = null //编译报错:Null can not be a value of a non-null type String
println(a.length)
二、可空类型:nullable
(一)声明nullable
的变量,需要使用?
修饰
当某个变量的值可以为null
的时候,必须在声明处的类型后添加?
来标识该引用可为空。
1、申明可空的变量
var a:String? = "danke"
a = null //ok
2、申明函数返回值为可空的变量
fun parseInt(str: String): Int? {
// ..
}
(二)无法直接使用nullable
的属性或者方法
可空变量是不安全的,直接调用方法和属性,可能会引起NPE
,所以编译器检测到了,那么编译失败
var a:String? = "danke"
println(a.length) // 编译失败,Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
(三)nullable
变量无法直接赋值给non-nullable
变量
可空变量无法直接赋值给非空变量,可能会引起NPE
,所以编译器检测到了,那么编译失败Type mismatch
var a:String? = "danke"
var b = "danke"
b = a //编译失败 Type mismatch: inferred type is String? but String was expected
三、如何解决nullable的使用?
(一)在条件中检查null
1、显示检查变量是否为null
,再进行逻辑处理。编译器会跟踪执行检查的信息,并允许在判空后(支持更复杂,更智能的条件),再调用变量的属性和方法
【变量处理】:
var a:String? = "danke"
if (a != null) {
println(a.length)
}
【使用返回可空值的函数】:
fun parseInt(str: String): Int? {
return str.toIntOrNull()
}
fun printProduct(arg1: String, arg2: String) {
val x = parseInt(arg1)
val y = parseInt(arg2)
// 直接使用 [x * y] 会导致编译错误,因为他们可能为 null
if (x != null && y != null) {
// 在空检测后,x 与 y 会自动转换为非空值(non-nullable)
println(x * y)
}
else {
println("either '$arg1' or '$arg2' is not a number")
}
}
2、只适用于当前变量不可变的情况(即在检查和使用之间没有修改过的局部变量 ,或者不可覆盖并且有幕后字段的 val 成员),因为否则可能会发生在检查之后 b 又变为null 的情况。否则需要再次在条件中检查null
(二)安全的调用:?.
1、使用安全调用操作符?.
表示如果非空那么调用属性或者方法,如果为空,则返回null
如果变量a非空,就返回a.length,否则返回null,a?.length的返回值类型为Int?
val a: String? = null
println(a?.length) // 打印null
2、链式调用中的使用:比如通过员工 bob, 获取他所在部门(department)的负责人(head)的名字(name),如果任意一个属性为空,那么这个链式调用返回空
var name = bob?.department?.head?.name
3、安全调用符?.
可以与let
一起使用,会忽略掉item为null的处理
var listHasNull = listOf("danke", null, "outa")
for (item in listHasNull) {
item?.let {
println(it) // 忽略null
}
}
打印结果:
4、安全调用符?.
可以出现在赋值的左侧,如果调用链中的任何一个属性为空,都会跳过这个赋值操作,表示右侧的表达式根本不会求值
bob?.department?.head?.name = getName()
(三)Elvis 操作符:?:
1、获取字符串str的长度,如果str为空则返回-1
(1)使用if-else
表达式
var str: String? = null
val len: Int = if (str != null) str.length else -1
使用if-else
表达式,会有提示建议使用Elvis 操作符
(2)使用Elvis 操作符:?:
,如果?:
的左侧表达式非空,那么就会返回?:
左侧表达式,否则返回右侧表达式(当且仅当左侧为空时,才会执行右侧的表达式)
var str: String? = null
val len: Int = str?.length ?: -1
2、Elvis 操作符结合throw和return使用
在Kotlin中,throw
和return
都是表达式,所以它们也可以使用在?:
的右侧
【例子】获取当前菜Instance的父菜的id,并且检查当前菜和父菜是否为空,父菜的唯一标识id,是否为空
class Instance {
fun getParent(): Instance? {
return Instance()
}
fun getId():String? {
return null
}
}
fun foo(instance: Instance?): String? {
val parent = instance?.getParent() ?: return null // 如果instance为空,或者instance.getParent()为空,都会返回null
val id = parent.getId() ?: throw IllegalArgumentException("id expected")
return "parentId: $id"
}
(四)操作符:!!
非空断言运算符!!
将任何值转换为非空类型,若该值为空则抛出异常KotlinNullPointerException
var str: String? = null
println(str!!.length) // 如果str == null时,运行报错kotlin.KotlinNullPointerException
(五)安全的类型转换:as?
1、类型转换时,如果对象不是目标类型,则会报ClassCastException
2、使用安全的类型转换as?
,如果尝试转换不成功返回null
var a = "123"
var aInt: Int? = a as? Int
println(aInt)
(六)可空类型的集合:filterNotNull
使用filterNotNull
可以过滤掉可空类元素集合中的空元素
var listHasNull = listOf("danke", null, "outa")
val listNotNull: List<String> = listHasNull.filterNotNull()
listNotNull.forEach {
println(it)
}
打印结果:
四、Kotlin可能中会出现的NPE
1、显示调用 throw NullPointerException()
2、使用了非空断言运算符!!
3、数据在初始化时不一致:
(1)传递一个在构造函数中出现的未初始化的this
,并用于其他地方(“泄漏this
”)
(2)父类的构造函数调用一个开放成员,该成员在子类的实现使用了未初始化的状态
4、Java互操作
(1)企图访问平台类型
的 null 引用的成员;
(2)用于具有错误可空性的 Java 互操作的泛型类型,例如一段 Java 代码可能会向Kotlin 的 MutableList<String>
中加入 null ,这意味着应该使用MutableList<String?>
来处理它;
(3)由外部 Java 代码引发的其他问题。