06. Kotlin 空安全和异常机制

null 安全

为了消除 NullPointerException,Kotlin 的变量类型不允许赋值 null。如果您需要一个可以为空的变量,可以通过添加 ?在其类型的末端表示为可空类型。

1
2
3
4
5
6
7
8
var neverNull: String = "This can't be null"
neverNull = null

var nullable: String? = "You can keep a null here"
nullable = null

var inferredNonNull = "The compiler assumes non-null"
inferredNonNull = null

手动管理 null

当你要一个可空类型变量做事,而它又可能不存在,对于这种潜在危险,编译器时刻警惕着。为了应对这种风险,Kotlin 不允许你在可空类型值上调 用函数,除非你主动接手安全管理。

使用 if 判断 null 值情况

安全处理 null 值的第三个办法是执行 if 条件表达式,判断变量是否为 null 值。

那么什么时候该用 if/else 语句做 null 值检查呢?如果需要一些复杂的逻辑运算才能判断某个 变量是否为 null,选择它就最合适。而且,使用 if/else 语句组织复杂逻辑,代码看起来会更清晰可读。

1
2
3
4
5
6
7
fun describeString(maybeString: String?): String {
if (maybeString != null && maybeString.length > 0) {
return "String of length ${maybeString.length}"
} else {
return "Empty or null string"
}
}

安全调用操作符

编译器看到有安全调用操作符,所以它知道如何检查 null 值。如果遇到 null 值,它就跳过函数调用,而不是返回 null。

1
var beverage = readLine()?.capitalize()

使用带 let 的安全调用

安全调用允许在可空类型上调用函数。但是,如果还想做点额外的事,比如创建新值,或判 断变量不为 null 就调用其他函数,那该怎么办呢?使用带 let 函数的安全调用操作符是个办法。 你可以在任何类型上调用 let 函数,它的主要作用是让你在指定的作用域内定义一个或多个变量。使用 let 的第二个好处体现在后台:let 会隐式返回表达式结果。这样,在表达式有了结果值后,你就能把结果赋值给一个变量。

1
2
3
4
5
6
7
var beverage = readLine()?.let {
if (it.isNotBlank()) {
it.capitalize()
} else {
"Buttered Ale"
}
}

使用 !!. 操作符

!!. 操作符也能用来在可空类型上调用函数。但我要给你个警告:相比安全调用操作符,!!. 操作符太激进,一般应该避免使用。视觉上看,代码中的 !!. 操作符也给人语气很重的感觉,因为它真的很危险。如果你用了 !!.,就是在向编译器宣布:“万一我使唤干活的东西不存在,我 要求你立刻抛出空指针异常!!”(顺便提一下,!!.的官方名字是非空断言操作符,但开发人员 更喜欢叫它“感叹号操作符”。)

尽管我们通常不建议你使用 !!. 操作符,但只要你心中有数,试一试也无妨。

使用空合并操作符

另一种检查 null 值的方式是使用 Kotlin 的空合并操作符?:(由于很像歌手埃尔维斯·普雷斯 利的标志性发型,又叫作“Elvis 操作符”)。这种操作符的意思是,“如果我左边的求值结果是 null,就使用右边的结果值”。

空合并操作符能避免 null 值的情况出现,只要首选项结果为空,就使用默认值赋值。另外, 编程时,如果某个不能为空的变量一时没想好怎么处理,也可以用它来做个缓冲,让你有时间慢慢思考。

异常类

kotlin 中的所有异常类都继承 Throwable 类。每个异常都有一条消息、一个堆栈跟踪和一个可选原因。

要抛出异常对象,请使用throw表达式:

1
throw Exception("Hi There!")

Throwable 类

所有的异常类都直接或间接地继承于 Kotlin.lang.Throwable 类,在 Throwable 类有几个非常重要的属性和函数:

  • message 属性。获得发生错误或异常的详细消息。
  • printStackTrace 函数。打印错误或异常堆栈跟踪信息。
  • toString 函数。获得错误或异常对象的描述。

Throwable 有两个直接子类:Error 和 Exception。

Error 是程序无法恢复的严重错误,程序员根本无能为力,只能让程序终止。例如:Java虚拟机内部错误、内存溢出和资源耗尽等严重情况。

Exception 是程序可以恢复的异常,它是程序员所能掌控的。例如:除零异常、空指针访问、网络连接中断和读取不存在的文件等。本章所讨论的异常处理就是对 Exception 及其子类的异常处理。

捕获异常

try-catch 结构

捕获异常是通过 try-catch 语句实现的,最基本 try-catch 语句语法如下:

1
2
3
4
5
6
7
try {
// some code
} catch (e: SomeException) {
// 处理异常 e
} finally {
// optional finally block
}

在 Kotlin 中 try-catch 语句很多情况下使用 try-catch 表达式代替,Kotlin 也提倡 try-catch 表达式写法,这样会使代码更加简洁。

可以是零个或更多 catch 块和 finally 可以省略方框。但是,至少有一个捕获或终于块是必需的。

如果 try 代码块中有很多语句会发生异常,而且发生的异常种类又很多。那么可以在 try 后面跟有多个 catch 代码块。

有时在 try-catch 语句中会占用一些非 Java 虚拟机资源,如:打开文件、网络连接、打开数据库连接和使用数据结果集等,这些资源并非 Kotlin 资源,不能通过 Java 虚拟机的垃圾收集器回收,需要程序员释放。为了确保这些资源能够被释放可以使用 finally 代码块或自动资源管理(Automatic Resource Management)技术。

try 是一个表达式,这意味着它可以有一个返回值:

1
val a: Int? = try { input.toInt() } catch (e: NumberFormatException) { null }

对象的返回值 try 表达式是中的最后一个表达式 try 块中的最后一个表达式 catch 块(或多个块)。的内容 finally 块不影响表达式的结果。

自动资源管理技术

当使用传统 finally 代码块释放资源会导致程序代码大量增加,一个 finally 代码块往往比正常执行的程序还要多。在 Kotlin 中可以使用Java 7 之后提供自动资源管理(Automatic Resource Management)技术,可以替代 finally 代码块,优化代码结构,提高程序可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private fun readDate(): Date? {
// 自动资源管理
try {
FileInputStream("readme.txt").use { fileis ->
InputStreamReader(fileis).use { isr ->
BufferedReader(isr).use { br ->
// 读取文件中的一行数据
val str = br.readLine() ?: return null
val df = SimpleDateFormat("yyyy-MM-dd")
return df.parse(str)
}
}
}
} catch (e: IOException) {
println("处理IOException...")
e.printStackTrace()
} catch (e: ParseException) {
println("处理ParseException...")
e.printStackTrace()
}
return null
}

通过调用输入流 use 函数进行嵌套,这就是自动资源管理技术了,采用了自动资源管理后不再需要 finally 代码块,不需要自己close这些资源,释放过程交给了 Java 虚拟机。

throw 与显式抛出异常

本节之前读者接触到的异常都是由于系统生成的,当异常发生时,系统会生成一个异常对象,并将其抛出。但也可以通过throw语句显式抛出异常,语法格式如下:

1
throw Throwable 及其子类实例

自定义异常

自定义异常类一般需要提供两个构造方法,一个是无参数的构造方法,异常描述信息是空的;另一个是有一个字符串参数的构造方法,message是异常描述信息。

1
2
3
4
5
class MyException : Exception {
constructor() {
}
constructor(message: String) : super(message) {}
}

先决条件函数

未预料到的值会导致出人意料的程序行为。作为开发者,为了确保某个输入值有效合法,你会 花费不少时间。有些异常来源很常见,比如未预料到的 null 值。为了方便验证输入或调试以避开常 见问题,Kotlin 标准库提供了一些便利函数。使用这些内置函数,你可以抛出带自定义信息的异常。

这样的便利函数又叫作先决条件函数(precondition function),因为你可以用它定义先决条件——条件必须满足,目标代码才能执行。

1
2
3
fun proficiencyCheck(swordsJuggling: Int?) {
checkNotNull(swordsJuggling, { "Player cannot juggle swords" })
}

对于先明确要求,再决定是否执行某段代码的场景,先决条件函数非常适合。相比于手动抛 出自定义异常,这种方式更简洁,因为要满足什么条件,看函数名就知道了。就上例来说,结果 都一样,也就是保证 swordsJuggling 不为 null,否则就抛出自定义异常,打印控制台信息。显 然,这里的 checkNotNull 函数要比前面抛出 UnskilledSwordJugglerException 异常更简洁。

Kotlin 标准库里内置了 5 个先决条件函数。这些函数的区别很大,提供了多样化的选择。

checkNotNull - 如果参数值为 null,则抛出 IllegalStateException 异常,否则返回非 null 值
require - 如果参数值为 false,则抛出 IllegalArgumentException 异常
requireNotNull - 如果参数值为 null,则抛出 IllegalStateException 异常,否则返回非 null 值
error - 如果参数值为 null,则抛出 IllegalStateException 异常并输出错误信息,否则返回非 null 值
assert - 如果参数值为 false,则抛出 AssertionError 异常,并打上断言编译器标记

这 5 个先决条件函数中,require 函数尤其有用。其他函数可以利用它指定自身值参的边界。

Nothing 类型