Android第一行代码——快速入门 Kotlin 编程(2.7空指针检查)

本文介绍了Kotlin如何通过编译时检查几乎消除空指针异常,详细讨论了可控类型系统和判空辅助工具,如? .操作符和? :操作符的使用,以及let函数在处理全局变量判空问题上的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

2.7        空指针检查

2.7.1        可控类型系统

 2.7.2        判空辅助工具


2.7        空指针检查

        我之前看过某国外机构做的一个统计,Android 系统上崩溃率最高的异常类型就是空指针异常 (NullPointerException )。相信不只是 Android ,其他系统上也面临着相同的问题。若要分析其根本原因的话,我觉得主要是因为空指针是一种不受编程语言检查的运行时异常,只能由程序员主动通过逻辑判断来避免,但即使是最出色的程序员,也不可能将所有潜在的空指针异常全部考虑到。

        我们来看一段非常简单的Java 代码:

public void doStudy(Study study) { 
    study.readBooks(); 
    study.doHomework(); 
} 

        这是我们在 2.5.3 小节编写过的一个 doStudy() 方法,我将它翻译成了Java 版。这段代码没有任何复杂的逻辑,只是接收了一个 Study 参数,并且调用了参数的 readBooks()doHomework() 方法。

        这段代码安全吗?不一定,因为这要取决于调用方传入的参数是什么,如果我们向 doStudy() 方法传入了一个 null 参数,那么毫无疑问这里就会发生空指针异常。因此,更加稳妥的做法是在调用参数的方法之前先进行一个判空处理,如下所示:

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

        这样就能保证不管传入的参数是什么,这段代码始终都是安全的。

        由此可以看出,即使是如此简单的一小段代码,都有产生空指针异常的潜在风险,那么在一个大型项目中,想要完全规避空指针异常几乎是不可能的事情,这也是它高居各类崩溃排行榜首 位的原因。

2.7.1        可控类型系统

        然而,Kotlin 却非常科学地解决了这个问题,它利用编译时判空检查的机制几乎杜绝了空指针异常。虽然编译时判空检查的机制有时候会导致代码变得比较难写,但是不用担心,Kotlin 提供了一系列的辅助工具,让我们能轻松地处理各种判空情况。下面我们就逐步开始学习吧。

        还是回到刚才的 doStudy() 函数,现在将这个函数再翻译回 Kotlin 版本,代码如下所示:

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

        这段代码看上去和刚才的 Java 版本并没有什么区别,但实际上它是没有空指针风险的,因为 Kotlin 默认所有的参数和变量都不可为空,所以这里传入的 Study 参数也一定不会为空,我们可以放心地调用它的任何函数。如果你尝试向 doStudy() 函数传入一个 null 参数,则会提示如 图2.30 所示的错误。

图2.30        向 doStudy() 方法传入 null 参数

        也就是说,Kotlin 将空指针异常的检查提前到了编译时期,如果我们的程序存在空指针异常的风险,那么在编译的时候会直接报错,修正之后才能成功运行,这样就可以保证程序在运行时期不会出现空指针异常了。

        看到这里,你可能产生了巨大的疑惑,所有的参数和变量都不可为空?这可真是前所未闻的事情,那如果我们的业务逻辑就是需要某个参数或者变量为空该怎么办呢?不用担心,Kotlin 提供 了另外一套可为空的类型系统,只不过在使用可为空的类型系统时,我们需要在编译时期就将所有潜在的空指针异常都处理掉,否则代码将无法编译通过。

        那么可为空的类型系统是什么样的呢?很简单,就是在类名的后面加上一个问号。比如,Int表示不可为空的整型,而 Int? 就表示可为空的整型;String 表示不可为空的字符串,而 String?就表示可为空的字符串。

        回到刚才的 doStudy() 函数,如果我们希望传入的参数可以为空,那么就应该将参数的类型由 Study 改成 Study?,如 图2.31 所示。

图2.31        允许 Stydy 参数为空

        可以看到,现在在调用 doStudy() 函数时传入 null 参数,就不会再提示错误了。然而你会发 现,在 doStudy() 函数中调用参数的 readBooks() doHomework() 方法时,却出现了一个红色下滑线的错误提示,这又是为什么呢?

        其实原因也很明显,由于我们将参数改成了可为空的 Study? 类型,此时调用参数的 readBooks() doHomework() 方法都可能造成空指针异常,因此 Kotlin 在这种情况下不允许编译通过。

        那么该如何解决呢?很简单,只要把空指针异常都处理掉就可以了,比如做个判断处理,如下所示:

fun main(){
    doStudy(null)
}

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

        现在代码就可以正常编译通过了,并且还能保证完全不会出现空指针异常。

        其实学到这里,我们就已经基本掌握了 Kotlin 的可空类型系统以及空指针检查的机制,但是为了在编译时期就处理掉所有的空指针异常,通常需要编写很多额外的检查代码才行。如果每处检 查代码都使用 if 判断语句,则会让代码变得比较啰嗦,而且 if 判断语句还处理不了全局变量的判空问题。为此,Kotlin 专门提供了一系列的辅助工具,使开发者能够更轻松地进行判空处理,下面我们就来逐个学习一下。

小贴士:

问号 ? 可为空的类型

 2.7.2        判空辅助工具

        首先学习最常用的 ? . 操作符。这个操作符的作用非常好理解,就是当对象不为空时正常调用相应的方法,当对象为空时则什么都不做。比如以下的判空处理代码:

if (a != null) { 
 a.doSomething()
} 

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

a?.doSomething() 

        了解了 ? . 操作符的作用,下面我们来看一下如何使用这个操作符对 doStudy() 函数进行优化, 代码如下所示:

fun main(){
    doStudy(null)
}

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

        可以看到,这样我们就借助 ? . 操作符将 if 判断语句去掉了。可能你会觉得使用 if 语句来进行判空处理也没什么复杂的,那是因为目前的代码还非常简单,当以后我们开发的功能越来越复 杂,需要判空的对象也越来越多的时候,你就会觉得 ? . 操作符特别好用了。        

        下面我们再来学习另外一个非常常用的 ? : 操作符。这个操作符的左右两边都接收一个表达式, 如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。观察如下代码:

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

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

val c = a ?: b 

        接下来我们通过一个具体的例子来结合使用 ? .? : 这两个操作符,从而让你加深对它们的理解。

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

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

        由于文本是可能为空的,因此我们需要先进行一次判空操作,如果文本不为空就返回它的长 度,如果文本为空就返回0。

        这段代码看上去也并不复杂,但是我们却可以借助操作符让它变得更加简单,如下所示:

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

        这里我们将 ? . ? : 操作符结合到了一起使用,首先由于 text 是可能为空的,因此我们在调用它的 length 字段时需要使用 ? . 操作符,而当 text 为空时,text?.length 会返回一个 null 值, 这个时候我们再借助 ? : 操作符让它返回 0。怎么样,是不是觉得这些操作符越来越好用了呢?

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

        观察如下的代码示例:

var content:String? = "hello"

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

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

        这里我们定义了一个可为空的全局变量 content,然后在 main() 函数里先进行一次判空操作,当 content 不为空的时候才会调用 printUpperCase() 函数,在 printUpperCase() 函数里,我们将 content 转换为大写模式,最后打印出来。

        看上去好像逻辑没什么问题,但是很遗憾,这段代码一定是无法运行的。因为 printUpperCase() 函数并不知道外部已经对 content 变量进行了非空检查,在调用 toUpperCase() 方法时,还认为这里存在空指针风险,从而无法编译通过。

        在这种情况下,如果我们想要强行通过编译,可以使用非空断言工具,写法是在对象的后面加上 ! !,如下所示:

var content:String? = "hello"

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

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

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

        虽然这样编写代码确实可以通过编译,但是当你想要使用非空断言工具的时候,最好提醒一下自己,是不是还有更好的实现方式。你最自信这个对象不会为空的时候,其实可能就是一个潜 在空指针异常发生的时候。

        最后我们再来学习一个比较与众不同的辅助工具—— letlet 既不是操作符,也不是什么关键 字,而是一个函数。这个函数提供了函数式 API 的编程接口,并将原始调用对象作为参数传递到 Lambda 表达式中。示例代码如下

obj.let { obj2 -> 
    // 编写具体的业务逻辑
} 

        可以看到,这里调用了obj 对象的 let 函数,然后 Lambda 表达式中的代码就会立即执行,并且这个 obj 对象本身还会作为参数传递到 Lambda 表达式中。不过,为了防止变量重名,这里我将参数名改成了obj2,但实际上它们是同一个对象,这就是 let 函数的作用。

        let 函数属于 Kotlin 中的标准函数,在下一章中我们将会学习更多 Kotlin 标准函数的用法。

        你可能就要问了,这个 let 函数和空指针检查有什么关系呢?其实 let 函数的特性配合 ? . 操作符可以在空指针检查的时候起到很大的作用。

        我们回到 doStudy() 函数当中,目前的代码如下所示:

fun main(){
    doStudy(null)
}

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

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

fun main(){
    doStudy(null)
}

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

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

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

fun main(){
    doStudy(null)
}

fun doStudy(study: Study?){
    study?.let { stu ->
        stu.readBooks()
        stu.doHomework()
    }
}

        我来简单解释一下上述代码,? . 操作符表示对象为空时什么都不做,对象不为空时就调用 let 函数,而 let 函数会将 study 对象本身作为参数传递到 Lambda 表达式中,此时的 study 对象肯 定不为空了,我们就能放心地调用它的任意方法了。

        另外还记得 Lambda 表达式的语法特性吗?当 Lambda 表达式的参数列表中只有一个参数时,可以不用声明参数名,直接使用 it 关键字来代替即可,那么代码就可以进一步简化成:

fun main(){
    doStudy(null)
}

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

        在结束本小节内容之前,我还得再讲一点,let 函数是可以处理全局变量的判空问题的,而 if 判断语句则无法做到这一点。比如我们将 doStudy() 函数中的参数变成一个全局变量,使用 let 函数仍然可以正常工作,但使用 if 判断语句则会提示错误,如 图2.32 所示。

图2.32        使用 if 判断语句对全局变量进行判空

        之所以这里会报错,是因为全局变量的值随时都有可能被其他线程所修改,即使做了判空处 理,仍然无法保证 if 语句中的 study 变量没有空指针风险。从这一点上也能体现出 let 函数的 优势。

        好了,最常用的 Kotlin 空指针检查辅助工具大概就是这些了,只要能将本节的内容掌握好,你就可以写出更加健壮、几乎杜绝空指针异常的代码了。

小贴士:

? . 操作符。当对象不为空时正常调用相应的方法,当对象为空时则什么都不做。

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

let 函数是可以处理全局变量的判空问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值