在任何编程语言中,空指针异常高居各类崩溃排行榜榜首!!!主要是因为空指针是一种不受编程语言检查的运行时异常,只能由程序员主动通过逻辑判断来避免,
Kotlin科学的解决了这个问题,在编译时的判空检查机制几乎杜绝了空指针异常。
1.1 可空类型系统
声明:下面代码的Study 为自行创建的一个接口Study,代码如下:
interface Study {
fun readBooks()
fun doHomeWork(){
println("do homework default implementation!")
}
}
首先,Kotlin是默认所有的参数和变量都不为空!!!在编译时,不能直接在函数中传入一个null参数,会在编译时直接报错。例如:
fun main(){
doStudy(null)
}
fun doStudy(study: Study) {
study.doHomeWork()
study.readBooks()
}
doStudy在编译时会报错,不允许传入null空指针。
但是,所有的变量都不可为空,会导致当我们的业务逻辑需要某个参数或变量为空时,不能实现代码,所以Kotlin提供了可空类型系统。
可空类型系统:在类名后加一个? 表示,该参数或变量可为空。刚才的例子修改为如下:
fun main(){
doStudy(null)
}
fun doStudy(study: Study?) {
study.doHomeWork()
study.readBooks()
}
但是,这时会出现一个情况,doStudy()中的函数会报错,因为当传入空指针时,接口中的方法可能会造成空指针异常,不可实现。因此需要进行判空处理,即:
fun doStudy(study: Study?) {
if (study != null) {
study.doHomeWork()
study.readBooks()
}
}
但是,为了在编译时处理掉所有的空指针异常,需要很多行检查代码,每次检查都需要一个if判空语句,代码会变得繁琐,也有可能会遗忘掉某些判空处理。Kotlin提供了一系列的辅助工具帮助进行判空处理。
1.2 判空辅助工具
首先、学习使用 ?. :当对象不为空时,正常调用相应方法。
例如下列代码:
if (a != null) {
a.doSomething()
}
可以改变为:
a?.doSomething()
根据 ?. 可以对doStudy函数进行优化,进一步简化了代码,如下:
fun doStudy(study: Study?) {
study?.doHomeWork()
study?.readBooks()
}
另外学习一个 ?: 操作符,在操作符左右都接收一个表达式,如果左边表达式的结果不为空则返回左边结果,否则返回右边表达式结果。
正常代码如下:
val c = if(a != null){
a
}else{
b
}
使用 ?: 操作符可简化为:
val c = a ?: b
一个具体的例子,获取某个字符串的长度,正常代码可以写为;
fun getStringLength(text: String?) :Int {
if (text != null) {
return text.length
}
return 0
}
根据判空操作符优化,以及语法糖,可以更改为:
fun getStringLength(text: String?) = text?.length ?: 0
在该函数中,text可能为空,因此输入参数时需要判空,执行函数体时,由于输入的参数text会出现为空的情况,因此需要使用 ?. 来判断是否为空。然后借助 ?: 来实现当text为空时返回0的情况。
不过有时,我们已经做了空指针异常处理,但是Kotlin编译器并不知道,还是会编译失败,代码如下:
var content: String? = "hello"
fun main() {
if (content != null) {
printUpperCase()
}
}
fun printUpperCase() {
val upperCase = content.uppercase()
println(upperCase)
}
全局变量content可能为空,在主函数中,先对content进行了判空处理,当不为空时,执行某函数。到了函数内部,调用uppercase()函数,Kotlin会认为这里存在空指针风险,编译器不知道我们在主函数内进行了判空,因此编译无法通过。如果想强行编译,可以这样写:
val upperCase = content!!.uppercase()
!! 是告诉Kotlin 我肯定这里的对象不为空,不用编译器做编译检查了,出现的后果自行承担。但是是一个非常危险的操作。这有更好的实现方式,使用一个不同的工具-------let函数。
let函数提供了函数式API编程接口,并将原始调用对象作为参数传递到Lambda表达式中。
obj.let { obj2 ->
//编写具体业务逻辑
}
调用let函数,会将对象本身作为参数传递到Lambda表达式中。
根据let函数,上述printUpperCase()可以写成:
fun printUpperCase() {
println(content?.let { it.uppercase() })
}
还有一点,使用let函数能够处理全局变量的判空问题,而if判断语句无法做到这一点,比如我们将doStudy()中的函数参数变为全局变量,使用if语句,会报错,使用let函数能够正常运行。如下:
因为全局变量的值可能被其他线程修改,可以将var,改为val,也可以使用let函数来对stu进行判空:
这样就不用考虑其他线程修改的问题了。