Kotlin系列三:空指针检查

1,833 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

Android系统上崩溃率最高的异常类型就是空指针异常(NullPointerException)。

public void doStudy(Study study) {
    if (study != null) {
        study.readBooks();
        study.doHomework();
    }
}

这种java里常见的判空检查容易陷入判空地狱的灾难。Kotlin提供了很好的解决思路。

1 可空类型(?)

Kotlin在编译时就进行判空检查,这会导致代码变得相对难写些,因为你得实时考虑到对象的为空与否。

一个判空举例:

fun doStudy(study: Study) {
    study.readBooks()
    study.doHomework()
}

如果你尝试向doStudy()函数传入一个null参数,则会提示错误:

那么可为空的类型系统是什么样的呢?很简单,就是在类名的后面加上一个问号:

为什么会出现红色报错:由于我们将参数改成了可为空的Study?类型,此时调用参数的readBooks()和doHomework()方法都可能造成空指针异常过。如何解决呢:

fun doStudy(study: Study?) {
    if (study != null) {
        study.readBooks()
        study.doHomework()
    }
}

2 判空辅助工具

2.1 ?.操作符

?.操作符:当对象不为空时正常调用相应的方法,当对象为空时则什么都不做(相当于外部包裹了 !=null 的一个判断了):

fun doStudy(study: Study?) {
    study?.readBooks()
    study?.doHomework()
}

2.1 ?:操作符

?:操作符:操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。

val c = if (a ! = null) {
    a
} else {
    b
}

这段代码的逻辑使用?:操作符就可以简化成:

val c = a ?: b

比如现在我们要编写一个函数用来获得一段文本的长度,使用传统的写法就可以这样写:

fun getTextLength(text: String?): Int {
    if (text != null) {
        return text.length
    }
    return 0
}

改进:

fun getTextLength(text: String?) = text?.length ?: 0

2.2 !!操作符

不过Kotlin的空指针检查机制也并非总是那么智能,有的时候我们可能从逻辑上已经将空指针异常处理了,但是Kotlin的编译器并不知道,这个时候它还是会编译失败。

观察如下的代码示例:

var content: String? = "hello"

fun main() {
    if (content != null) {
        printUpperCase()
    }
}

fun printUpperCase() {
    val upperCase = content.toUpperCase()
    println(upperCase)
}

看上去好像逻辑没什么问题,但这段代码一定是无法运行的。因为printUpperCase()函数并不知道外部已经对content变量进行了非空检查,在调用toUpperCase()方法时,还认为这里存在空指针风险,从而无法编译通过。在这种情况下,如果我们想要强行通过编译,可以使用非空断言工具,写法是在对象的后面加上!!,如下所示:

fun printUpperCase() {
    val upperCase = content!!.toUpperCase()
    println(upperCase)
}

这种写法意在告诉Kotlin,我非常确信这里的对象不会为空,所以不用你来帮我做空指针检查了,如果出现问题,你可以直接抛出空指针异常,后果由我自己承担。

2.3 let函数

let函数属于Kotlin中的标准函数,这个函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到Lambda表达式中。示例代码如下:

obj.let { obj2 ->
    // 编写具体的业务逻辑
}
结合doStudy()函数:

fun doStudy(study: Study?) {
    study?.readBooks()
    study?.doHomework()
}

虽然这段代码我们通过?.操作符优化之后可以正常编译通过,但其实这种表达方式是有点啰嗦的,如果将这段代码准确翻译成使用if判断语句的写法,对应的代码如下:

fun doStudy(study: Study?) {
    if (study != null) {
        study.readBooks()
    }
    if (study != null) {
        study.doHomework()
    }
}

也就是说,本来我们进行一次if判断就能随意调用study对象的任何方法,但受制于?.操作符的限制,现在变成了每次调用study对象的方法时都要进行一次if判断。

这个时候就可以结合使用?.操作符和let函数来对代码进行优化了,如下所示:

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()
    }
}

主要参考

郭霖《第一行代码》 Kotlin部分学习记录