null安全可以说是Kotlin语言对Java的重大改进之一,这样可以避免Java编程时令人恐惧的“NullPointerException”(简称:NPE)。但话说回来,null安全不过是各种现代语言玩剩下的东西。
1 非空类型和可空类型
下面先看一个简单的例子。
程序清单:codes\02\2.8\NullableTest.kt
var str = "fkit"
// 由于str转换为Int有可能失败,故num有可能没有值
// 因此不能使用Int来声明num的类型
var num: Int = str.toIntOrNull()
var num: Int? = str.toIntOrNull()
println(num)
}
对比上面两行粗体字代码:第一行代码声明num的类型为Int,第二行代码声明num的类型为Int?。程序中第一行粗体字代码无法通过编译,第二行粗体字代码能通过编译。其中Int?就是可空类型,这种类型的变量可接受Int值和null两种情况;而Int类型的变量则只接受Int值,不能接受null的情况。由于str是一个String变量,当程序试图把String变量转换为Int值时,有可能转换成功(如果变量值是形如"123"的字符串),也有可能转换失败(本程序就转换失败了)。转换失败时,就无法成功返回Int值,此时将会返回null,因此必须使用Int?类型的变量来存储转换结果。
对于可能发生“值缺失”的情况,编译器会自动推断该变量的类型为可空类型。例如如下代码:
var n = str.toIntOrNull()
需要指出的是,只有可空类型的变量或常量才能接受null,非空类型的变量和常量不能接受null。
Kotlin对可空类型进行了限制:如果不加任何处理,可空类型不允许直接调用方法、访问属性。因此,通过可空类型与非空类型的区分,Kotlin即可在程序中避免空指针异常。例如如下程序(程序清单同上):
var bStr: String? = "fkit"
aStr = null // 错误,aStr不接受null值
bStr = null // 正确
// 编译通过,aStr不可能为null,运行时不可能导致NPE
println(aStr.length)
// 编译不能通过,不可能导致NPE
println(bStr.length)
上面代码中定义了aStr和bStr两个变量,其中aStr是非空String类型,因此aStr变量不允许赋值为null;而bStr是可空的String类型,bStr变量允许被赋值为null;程序可以直接调用aStr的方法和属性,但aStr变量的值不可能为nul的,因此可以避免NPE;Kotlin对可空类型进行了限制,可空类型不允许直接调用方法和属性,因此程序不能直接调用bStr的方法或属性,这样也可避免NPE。
2 先判断后使用
可空类型的变量不允许直接调用方法或属性,但可以先判断该变量不为null,然后再去调用该变量的属性和方法。例如如下代码。
程序清单:codes\02\2.8\CheckNull.kt
f
var b: String? = "fkit"
// 先判断b不为null,然后访问b的length属性
var len = if (b != null) b.length else -1
println("b的长度:${len}")
b = null
// 先判断b不为null,然后调用b的length属性
if (b != null && b.length > 0) {
// 访问b的length属性
println(b.length)
}
else {
println("空字符串")
}
}
上面程序定义了String?(可空类型)的变量b,这样程序不能直接调用变量b的方法和属性,Kotlin要求程序先判断b不为null,接下来程序即可在该条件下调用b的方法或属性。
对于非空Boolean类型而言,它可以接受3个值,true、false或null,因此对Boolean?类型变量进行判断时,需要使用Boolean?变量显式与true、false值进行比较。例如如下代码。
f
var b: Boolean? = null
if( b == true ){
println("为真")
}
}
如果将if分支改为如下形式:
if( b ){
println("为真")
}
编译器会报错:type mismatch: inferred type is Boolean? but Boolean was expected,这是因为Kotlin的if条件必须是Boolean类型,而Boolean?与Boolean本质上是两个不同类型,因此编译器会报错。
3 安全调用
安全调用则在Java中已出现很多年了,只不过没有出现在Java官方语法中——如果读者熟悉Spring EL,一定见过如下用法。
user?.dog?.name
上面表达式语言表示如果user不为null,则返回user的dog属性;如果dog属性值不为null,则继续获取dog属性值的name属性值。反过来,如果user为null,或user.dog为null,上面表达式语言都不会导致NPE,而是整个表达式返回null——这就是Spring EL的安全调用。
Kotlin的安全调用也完全与此类似,例如如下代码。
程序清单:codes\02\2.8\SafeCall.kt
var b: String? = "fkit"
println(b?.length) // 输出4
b = null
println(b?.length) // 输出null
}
上面程序中变量b的类型是String?,因此程序使用了?.安全调用来访问b的length属性,当b为null时,程序也不会引发NPE,而是返回null。
与Spring EL类似的是,Kotlin的安全调用完全也支持链式调用,就像Spring EL的用法一样:
user?.dog?.name
上面代码表示安全获取user的dog的name属性值,如果user或user.dog为null,整个表达式将会返回null。
此外,安全调用还可与let全局函数结合使用,例如如下代码(程序清单同上)。
val myArr: Array<String?> = arrayOf("fkit", "fkjava", null, "crazyit")
for (item in myArr) {
// 当item不为null时才调用let函数
item?.let { println(it) }
}
上面粗体字代码使用安全调用来调用let函数,这样只有当item元素不为null才会执行let函数。上面程序调用let函数时传入一个Lambda表达式作为函数参数。
4 Elvis运算
Elvis运算也是一种早就满大街的小技巧了,其实就是if else的简化写法。对比如下代码。
程序清单:codes\02\2.8\Elvis.kt
var b: String? = "fkit"
// 先判断b不为null,然后访问b的length属性
var len1 = if (b != null) b.length else -1
println(len1);
b = null
// 使用Elvis运算符
var len2 = b?.length ?: -1
println(len2);
}
上面第一行粗体字代码使用传统if分支进行判断,当b不为null时返回b.length,否则返回-1;第二行粗体字代码则使用?:运算符,该运算符就是Elvis——它的含义是:如果?:左边的表达式不为null时,则返回左边表达式的值,否则返回?:右边表达式的值。
由此可见?:其实就是if分支的简化写法。
此外,由于Kotlin的return、throw都属于表达式,因此它们也都可以用在?:运算符的右边。例如如下代码片段:
val data = ……
val email = data["email"] ?: throw IllegalArgumentException("没有指定Email信息!")
5 强制调用
强制调用时为NPE爱好者准备的——如果读者依然喜欢Java那种简单、粗暴的方式:不管变量是否为null,程序都直接调用该变量的方法或属性,Kotlin也为这种用法提供了支持,用!!.即可强制调用可空变量的方法或属性,这样强制调用可能引发NPE。例如如下代码。
程序清单:codes\02\2.8\ForceCall.kt
var b: String? = "fkit"
println(b!!.length) // 输出4
b = null
println(b!!.length) // 删除null
// 定义一个元素可空的数组
val myArr: Array<String?> = arrayOf("fkit", "fkjava", null, "crazyit")
for (item in myArr) {
// 当item不为null时才调用let函数
item!!.let { println(it) }
}
}
上面程序就是将前面的安全调用程序中?.安全调用该为!!.强制调用,当可空变量b为null时,b!!.length将会引发NPE;与前面安全调用类似的是,强制调用也可作用于let()函数,此时不管item元素是否为null,程序都会对该元素调用let()函数,因此也可能导致NPE。