06. Kotlin 空安全和异常机制
null 安全
为了消除 NullPointerException,Kotlin 的变量类型不允许赋值 null。如果您需要一个可以为空的变量,可以通过添加 ?在其类型的末端表示为可空类型。
1 | var neverNull: String = "This can't be null" |
手动管理 null
当你要一个可空类型变量做事,而它又可能不存在,对于这种潜在危险,编译器时刻警惕着。为了应对这种风险,Kotlin 不允许你在可空类型值上调 用函数,除非你主动接手安全管理。
使用 if 判断 null 值情况
安全处理 null 值的第三个办法是执行 if 条件表达式,判断变量是否为 null 值。
那么什么时候该用 if/else 语句做 null 值检查呢?如果需要一些复杂的逻辑运算才能判断某个 变量是否为 null,选择它就最合适。而且,使用 if/else 语句组织复杂逻辑,代码看起来会更清晰可读。
1 | fun describeString(maybeString: String?): String { |
安全调用操作符
编译器看到有安全调用操作符,所以它知道如何检查 null 值。如果遇到 null 值,它就跳过函数调用,而不是返回 null。
1 | var beverage = readLine()?.capitalize() |
使用带 let 的安全调用
安全调用允许在可空类型上调用函数。但是,如果还想做点额外的事,比如创建新值,或判 断变量不为 null 就调用其他函数,那该怎么办呢?使用带 let 函数的安全调用操作符是个办法。 你可以在任何类型上调用 let 函数,它的主要作用是让你在指定的作用域内定义一个或多个变量。使用 let 的第二个好处体现在后台:let 会隐式返回表达式结果。这样,在表达式有了结果值后,你就能把结果赋值给一个变量。
1 | var beverage = readLine()?.let { |
使用 !!. 操作符
!!. 操作符也能用来在可空类型上调用函数。但我要给你个警告:相比安全调用操作符,!!. 操作符太激进,一般应该避免使用。视觉上看,代码中的 !!. 操作符也给人语气很重的感觉,因为它真的很危险。如果你用了 !!.,就是在向编译器宣布:“万一我使唤干活的东西不存在,我 要求你立刻抛出空指针异常!!”(顺便提一下,!!.的官方名字是非空断言操作符,但开发人员 更喜欢叫它“感叹号操作符”。)
尽管我们通常不建议你使用 !!. 操作符,但只要你心中有数,试一试也无妨。
使用空合并操作符
另一种检查 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 | try { |
在 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 | private fun readDate(): Date? { |
通过调用输入流 use 函数进行嵌套,这就是自动资源管理技术了,采用了自动资源管理后不再需要 finally 代码块,不需要自己close这些资源,释放过程交给了 Java 虚拟机。
throw 与显式抛出异常
本节之前读者接触到的异常都是由于系统生成的,当异常发生时,系统会生成一个异常对象,并将其抛出。但也可以通过throw语句显式抛出异常,语法格式如下:
1 | throw Throwable 及其子类实例 |
自定义异常
自定义异常类一般需要提供两个构造方法,一个是无参数的构造方法,异常描述信息是空的;另一个是有一个字符串参数的构造方法,message是异常描述信息。
1 | class MyException : Exception { |
先决条件函数
未预料到的值会导致出人意料的程序行为。作为开发者,为了确保某个输入值有效合法,你会 花费不少时间。有些异常来源很常见,比如未预料到的 null 值。为了方便验证输入或调试以避开常 见问题,Kotlin 标准库提供了一些便利函数。使用这些内置函数,你可以抛出带自定义信息的异常。
这样的便利函数又叫作先决条件函数(precondition function),因为你可以用它定义先决条件——条件必须满足,目标代码才能执行。
1 | fun proficiencyCheck(swordsJuggling: Int?) { |
对于先明确要求,再决定是否执行某段代码的场景,先决条件函数非常适合。相比于手动抛 出自定义异常,这种方式更简洁,因为要满足什么条件,看函数名就知道了。就上例来说,结果 都一样,也就是保证 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 函数尤其有用。其他函数可以利用它指定自身值参的边界。