异常处理
Kotlin 的异常处理机制主要依赖于try、catch、finally、throw 这四个关键字。
- try 块里面放置可能引发异常的代码
- catch 后对应异常类型和一个代码块,表明该 catch 块用于处理这种类型的代码块
- finally 块,用于回收在 try 块里打开的物理资源,异常处理机制会保证 finally 块总被执行
- throw 用于抛出一个具体的异常对象
Kotlin 所有异常都是 runtime 异常,抛弃了 checked 异常
异常处理机制
使用 try...catch 捕获异常
语法结构:
try { //1 个
//业务实现代码
//...
} catch (e: Exception) { //0~N 个
//异常处理代码
//...
} finally { //0~1 个
//可选的 finally
//...
}
整个异常处理流程可包含 1 个问 try 块、 0~N 个 catch 块、0~1 个 finally 块,但 catch 块与 finally 块至少出现其中之一
如果在执行 try 块中的业务逻辑代码时出现异常,系统将自动生成一个异常对象,该异常对象被提交给运行时环境,这个过程被称为抛出(throw)异常
当运行时环境收到异常对象时,会寻找能处理该异常对象的 catch 块,如果找到了合适的 catch 块,就把该异常对象交给该 catch 块处理,这个过程被称为捕获(catch)异常
如果运行时环境找不到捕获异常的 catch 块,则运行时环境中止,程序也将退出
try {
val fis = FileInputStream("a.txt")
Log.d(TAG, "onCreate: ${fis.read()}")
} catch (e: Exception) {
Log.d(TAG, "onCreate: 读取文件出现异常")
}
如果 try 块中的某条语句引起了异常,该语句后的其他语句通常不会获得执行的机会,为了保证一定能回收 try 块中打开的物理资源,异常处理机制提供了 finally 块,不管 try 块中的代码是否出现异常,也不管哪一个 catch 块被执行,甚至在 try 块或 catch 块中执行了 return 语句, finally 块总会被执行,但如果使用 System.exit(1) 语句来退出虚拟机,则 finally 块将失去执行的机会
var fis: FileInputStream? = null
try {
fis = FileInputStream("a.txt")
Log.d(TAG, "onCreate: ${fis.read()}")
} catch (e: Exception) {
Log.d(TAG, "onCreate: 读取文件出现异常")
// return 语句强制方法返回,会执行下面的 finally 块
return
//注释掉 return,使用 exit 退出虚拟机,不会执行下面的 finally 块
//System.exit(1)
} finally {
//关闭磁盘文件,回收资源
fis?.close()
Log.d(TAG, "onCreate: 执行 finally 块里的资源回收!")
}
除非在 try 块、 catch 块中调用了退出虚拟机的方法,否则不管在 try 块、 catch块执行怎样的代码,finally块总会执行
如果在 finally 块中使用了 return 或 throw 语句,就将会导致 try 块和 catch 块中的 return、throw 语句失效
//整个方法hi返回false
fun test(): Boolean {
try {
//因为 finally 块中包含了 return 语句
//所以下面的 return 语句失去作用
return true
} finally {
return false
}
}
当程序执行 try 块、catch 块时遇到了 return 或 throw 语句,这两条语句都会导致该方法立即结束,但是系统执行这两条语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有 finally 块,程序将立即执行 return 或 throw 语句,方法中止;如果有 finally块,系统就立即开始执行 finally 块一一只有当 finally 块执行完成后,系统才会再次跳回来执行 try 块、 catch 块中的 return 或 throw 语句,如果 finally 块中也使用了 return 或 throw 等导致方法中止的语句,finally 块己经中止了方法,系统将不会跳回去执行 try 块、catch 块中的任何代码
异常类的继承体系
与 Java 的异常处理流程类似,当运行时环境接收到异常对象后,会依次判断该异常对象是否是 catch 块后异常类或其子类的实例,如果是,运行时环境将调用该 catch 块来处理该异常;否则,将再次拿该异常对象和下 catch 块中的异常类进行比较
所有的异常类都是 Throwable 类的子类,此外,Kotlin还通过类型别名的方式引入了 Java 的 Error 和 Exception 两个子类
程序总是把对应 Exception 类的 catch 块放在最后,如果吧 Exception 类对应的 catch 块排在其他 catch 块的前面, Kotlin 运行时将直接进入该 catch 块,而排在它后面 catch 块将永远不会执行,因此,要先捕获小异常,再捕获大异常
访问异常信息
如果需要在 catch 块中访问异常对象的相关信息,则可以通过访问 catch 块后的异常形参来获得
方法或属性 | 说明 |
---|---|
message | 该属性返回该异常的详细描述字符串 |
stackTrace | 该属性返回该异常的跟踪栈信息 |
printStackTrace() | 将该异常的跟踪栈信息输出到标准错误输出 |
printStackTrace(PrintStream s) | 将该异常的跟踪栈信息输出到指定输出流 |
try 语句是表达式
与 if 语句类似, Kotlin 的 try 语句也是表达式,因此也可用于对变量赋值,try 表达式的返回值是 try 块或被执行的 catch 块中的最后一个表达式的值, finally 块中的内容不会影响表达式的结果
var str = "A"
//用 try 表达式对变量 a 赋值,为null
val a: Int? = try {
Integer.parseInt(str)
} catch (e: NumberFormatException) {
null
}
Log.d(TAG, "onCreate: a=$a")
使用 throw 抛出异常
使用 throw 语句抛出异常,抛出的不是异常类,而是一个异常实例
fun throwChecked(a: Int) {
if (a > 0) {
//自行抛出普通异常,在Kotlin也不是checked异常,Kotlin只有runtime异常
//该代码不必处于 try 块中
throw Exception(" a 的值大于0,不符合要求")
}
}
自定义异常类
自定义异常都应该继承 Exception 基类,定义异常类时通常需要提供两个构造器:一个是无参数的构造器;另一是带一个字符串参数的构造器,这个 符串将作为该异常对象的描述信息(也就是异常对象的 message 属性的返回值)
class CustomException : Exception {
//无参数的构造器
constructor () {}
//一个字符串参数的构造器
constructor(msg: String) : super(msg) {}
}
catch 和 throw 同时使用
在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常
为了实现这种通过多个方法协作处理同一个异常的情形,可以在 catch 块中结合 throw 句来完成
class AuctionException : Exception {
//无参数的构造器
constructor () {}
//一个字符串参数的构造器
constructor(msg: String) : super(msg) {}
}
class AuctionTest {
var initPrice: Double = 30.0
fun bid(bidPrice: String) {
var d: Double
try {
d = bidPrice.toDouble()
} catch (e: Exception) {
//此处完成本方法中可以对异常执行的修复处理
//此处仅仅是在控制台打印异常的跟踪栈信息
e.printStackTrace()
//再次抛出自定义异常
throw AuctionException("竞拍价必须是数值,不能包含其他字符")
if (initPrice > d) {
throw AuctionException("竞拍价比起拍价低,不允许竞拍")
initPrice = d
}
}
}
}
//使用
val at = AuctionTest()
try {
at.bid("aa")
} catch (e: AuctionException) {
//再次捕获到 bid() 方法中的异常,并对该异常进行处理
println(e.message)
}
异常链
把底层的原始异常直接传给用户是一种不负责任的表现。通常的做法是:程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息
class SalException : Exception {
//无参数的构造器
constructor () {}
//一个字符串参数的构造器
constructor(msg: String) : super(msg) {}
}
fun calSal() {
try {
//实现计算工资的业务逻辑
} catch (sqle: SQLException) {
// 把原始异常记录下来,留给管理员
//...
//下面异常中的 message 就是对用户的提示
throw SalException("访问底层数据库出现异常")
} catch (e: Exception) {
// 把原始异常记录下来,留给管理员
//...
//下面异常中的 message 就是对用户的提示
throw SalException("系统出现未知异常")
}
}
捕获一个异常,然后抛出另一个异常,并把原始异常信息保存下来,这是一种典型的链式处理( 23 种设计模式之 :职责链模式),也被称为“异常链”
Kotlin 的 Throwable 类及其子类在构造器中都可以接收 cause 对象作为参数,这个cause 就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,我们也能通过这个异常链追踪到异常最初发生的位置
class SalException : Exception {
//无参数的构造器
constructor () {}
//一个字符串参数的构造器
constructor(msg: String) : super(msg) {}
//创建一个可以接收 Throwable 参数的构造器
constructor(t: Throwable) : super (t) {}
}
fun calSal() {
try {
//实现计算工资的业务逻辑
} catch (sqle: SQLException) {
// 把原始异常记录下来,留给管理员
//...
//下面异常中的 sqle 就是原始异常
throw SalException(sqle)
} catch (e: Exception) {
// 把原始异常记录下来,留给管理员
//...
//下面异常中的 e 就是原始异常
throw SalException(e)
}
}
throw 语句是表达式
与 try 语句是表达式一样, Kotlin 的 throw 语句也是表达式,但由于 throw 表达式的类型比较特殊,是 Nothing 类型,因此很少将 throw 语句赋值给其他变量,但我们可以在 Elvis 达式中使用 throw 表达式
class User(var name: String? = null)
fun test() {
val user = User()
// 在 Elvis 表达式中使用 throw 表达式
// throw 表达式表示程序出现异常,不会真正对变量赋值
val th: String = user.name ?: throw NullPointerException("目标对象不能为 null")
Log.d("TAG", "test: th=$th")
}
//使用
test()
throw 表达式的类型是 Nothing,用于表示程序无法“真正”得到该表达式的值,运行该程序,可看到程序因为 NullPointerException 异常而结束
异常的跟踪栈
异常对象的 printStackTrace() 方法用于打印异常的跟踪栈信息,根据 printStackTrace() 方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程,例如上面程序的异常跟踪栈:
Caused by: java.lang.NullPointerException: 目标对象不能为 null
at com.example.kotlin.MainActivityKt.test(MainActivity.kt:39)
at com.example.kotlin.MainActivity.onCreate(MainActivity.kt:24)
at android.app.Activity.performCreate(Activity.java:7136)
at android.app.Activity.performCreate(Activity.java:7127)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2893)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新抛出了新异常),异常就从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法的调用者再次传给其调用者……直至最后传到 main() 方法,如果 main() 方法依然没有处理该异常,该异常就会传给 Kotlin 运行时环境, Kotlin 运行时环境会中止该程序,并打印异常的跟踪找信息
第一行的信息显示了异常的类型和异常的详细消息
接下来跟踪栈记录了程序中所有的异常发生点,各行显示了被调用方法中执行停止的位置,并标明了类、类中的方法名、与故障点对应的文件的行数
虽然 printStackTrace() 方法可以很方便地用于跟踪异常的发生情况,可以调试程序,但在最后发布的程序中,应该避免使用它