空指针检查
- 空指针异常是一种不受编程语言检查的运行时异常,只能由程序员主动通过逻辑判断来避免,所以空指针异常往往比较容易出这个问题
可空类型系统
- 在Kotlin当中利用了编译时判空检查的机制几乎杜绝了空指针异常,Kotlin提供了一些列的工具,让我们能够轻松处理各种判空情况
fun doStudy(study: Study) {
study.readBooks()
study.doHomeWork()
}
- 上面这个代码如果使用Java语言来进行编写的话,当我们参数传递为null,那么下面调用两个方法就会爆出空指针异常,所以在调用方法之前应该检查一下参数是否为空,这样的话就不会出现空指针异常
fun doStudy(study: Study) {
if(study != null) {
study.readBooks()
study.doHomeWork()
}
}
- 但是实际上,在Kotlin当中,根本不用if来进行判断study参数是否为空,因为在Kotlin语法中,默认所有的参数和变量都是不可以为空的,所以这里传入的Study参数一定不会为空,我们可以放心的调用它的任何函数.
- 但是如果尝试向doStudy函数中传递一个空的Study参数,编译器就会提示一个如下的错误出现,也就是说,Kotlin将空指针的检查提前到了编译阶.
- 但是这个的前提是,Kotlin当中的所有的参数和变量都默认不为空,但是当我们的业务逻辑要求某个参数就是为空该怎么办呢?
- 不用担心在Kotlin当中为我们提供了另外一套可以为空的系统,只不过就是在使用可以为空的系统的时候,我们需要在编译阶段就将所有潜在空指针异常的地方处理掉,否者代码无法编译通过
- 所谓可以为空的系统激素hi在类名的后面加上一个?号,比如Int表示不可以为空的整形,Int?表示一个可以为空的整形
- 回到刚刚的doStudy()函数,如果我们希望传入的参数的可以为空,那么就将Study改成Study?
fun main() {
doStudy(null)
}
fun doStudy(study: Study?) {
study.readBooks()
study.doHomeWork()
}
- 可以看到调用doStudy()函数传入null的时候,不会再提示上面的错误了,意思就是现在Study这个参数可以为空了,但是发现在下面调用study参数的两个方法的时候,发生了报错
- 由于我们将参数改成了可空的Study?类型,此时调用readBooks()和doHomeWork()方法的时候都有可能会造成空指针,因此在这种情况下Kotlin不允许编译通过
- 改进代码就是
fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
study.doHomeWork()
}
}
- 为了能够在编译时期就处理掉所有的空指针异常,通常需要编写很多的额外代码才行,如果每处都是用if来进行判断,就会比较啰嗦,而且if语句还是处理不了全局变量的判空问题
- 为此Koltin提供了一系列的辅助工具,使开发者能够更轻松地进行空指针处理.
判空辅助工具
- 首先是最常用的?.操作符,这个操作符的作用是,当对象不为空时正常调用,当对象为空的时候什么也不干.
- 了解了?.操作符之后,doStudy()函数进行优化,代码如下
fun doStudy(study: Study?) {
study?.doHomeWork()
study?.readBooks()
}
- 还有一个非常常用的?:操作符,这个操作符的左右两边都接受一个表达式,当左边表达式不为空的时候,就输出左边表达式的值,否者就输出右边表达式的值
- 编写一段函数来输出一段文本的长度
fun getTextLength(text: String?) = text?.length ?: 0
- Kotlin的空指针检查机制也不是总是那么智能,有时候我们从逻辑上将空指针异常处理了,但是Kotlin的编译器并不知道,这个时候他还是会编译失败
val content: String? = "hello"
fun main() {
if(content != null) {
printUpperCase()
}
}
fun printUpperCase() {
val upperCase= content.toUpperCase()
println(upperCase)
}
- 在上述的代码中定义了一个全局变量content,在main函数当中我们在调用printUpperCase()函数之前就已经判断了content是否为空,所以在printUpperCase()函数中content不可能为空
- 但是很遗憾的是,这段代码一定是无法运行的,因为printUpperCase()函数并不知道外部已经对content变量进行非空检查,在调用toUpperCase()函数的时候,不知道外部已经将content变量进行了非空检查,所以content在调用toUpperCase()函数的时候还是会存在空指针异常.
- 在这种情况下,如果我们想要强行的通过编译,可以使用非空断言工具,写法是在对象的后面加上!!操作符
fun printUpperCase() {
val upperCase = content!!.toUpperCase()
println(upperCase)
}
- 这是一种有风险的写法,意在告诉Kotlin,我非常确信这里的对象不会为空,所以你就不用来帮我做空指针的检查了,如果出现空指针问题,你抛出空指针异常,后果由我来进行承担
- 所以这种写法是存在潜在的空指针问题的,如果有更好的写法不建议这样使用
- let工具.let既不是操作符,也不是关键字.而是一个函数.这个函数提供了函数式API接口,并将原始调用对象作为参数传递到Lambda表达式当中
obj.let { obj2 ->
}
- let结合?.操作符将doStudy()方法进行优化
fun doStudy(study: Study?) {
study?.let {stu ->
stu.readBooks()
stu.doHomeWork()
}
}
- ?.操作符表示对象对空的时候什么都不做,对象不为空的时候就调用let函数,而let函数会将study对象本身作为参数传递刀Lambda表达式当中,此时study对象肯定就不为空了,我们就可以放心的任意调用他的方法了
- Lambda表达式有一个特性就是说,当Lambda表达式的参数裂掉只有一个参数的时候,可以不声明参数的名字,直接使用it关键字来进行代替即可
fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomeWork()
}
}
- let函数可以处理全局变量的判空问题,而if语句无法做到这一点,比如我们将doStudy()函数中的参数变成一个全局变量,使用let函数仍然可以正常工作,但是使用if判断语句会提示报错
- 之所以在这个地方会报错是因为全局变量的值,随时有可能会被其他线程所修改,即使做了判空处理,任然无法保证study变量没有空指针风险,这一点也体现出了let函数的优势.