Kotlin 的异常处理机制主要依赖于try、catch、finally、throw 这四个关键字,其中try关键字后紧跟一个用花括号括起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码。 catch 后对应异常类型和一个代码块,表明该 catch 块用于处理这种类型的代码块。 多个catch块后还可以跟一个 finally块,用于回收在try块里打开的物理资源,异常处理机制会保证 finally块总被执行。 而 throw用于抛出一个实际的异常, throw可以单独作为语句使用,抛出一个具体的异常对象 。
与 Java 的异常处理机制相比, Kotlin 抛弃了checked 异常,相当于所有异常都是 runtime 异常,这意味着开发者想捕获异常就捕获,不想捕获异常也行,不需要使用 throws 关键字声明抛出异常。
异常处理机制
使用 try...catch 捕获异常
Kotlin 的异常处理机制的语法结构如下:
try{
//业务实现代码
...
}catch(e:Exception){
//异常处理代码
...
}
...
finally{
//可选的finally 块
}
与Java的异常处理流程相同,整个异常处理流程可包含 1个try块、0~N个catch块、 0~1个 finally块,但 catch块与 finally块至少出现其中之一。
如果在执行try块中的业务逻辑代码时出现异常,系统将自动生成一个异常对象,该异常对象被提交给运行时环境,这个过程被称为抛出( throw)异常。
当运行时环境收到异常对象时,会寻找能处理该异常对象的 catch块,如果找到了合适的catch块,就把该异常对象交给该 catch 块处理,这个过程被称为捕获( catch)异常;如果运行时环境找不到捕获异常的catch块,则运行时环境中止,程序也将退出 。
import java.io.FileInputStream
import java.io.IOException
fun main(args: Array<String>) {
var fis:FileInputStream? = null
try{
fis = FileInputStream("a.txt")
println(fis.read())
}catch (e:IOException){
println("读取文件出现异常")
//return 语句强制方法返回
//return
//使用 exit 退出虚拟机
//System.exit(1)
}finally {
//关闭磁盘文件,回收资源
fis?.close()
println("执行 finally 块里的资源回收!")
}
}
上面程序在try块后增加了 finally 块,用于回收在try块中打开的物理资源 。 注意程序的 catch块中有一条return语句,该语句强制方法返回。 在通常情况下, 一旦在方法中执行到return语句的地方, 程序将立即结束该方法; 而现在不会了,虽然return语句也强制方法结束,但一定会先执行 finally块中的代码。
在异常处理的 catch 块中使用 System.exit(1)语句来退出虚拟机。运行上面代码,可以发现finally块没有被执行。 如果在异常处理代码中使用 System.exit(1)语 句来退出虚拟机,则finally块将失去执行的机会。
当程序执行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块中的异常类进行比较。
当程序进入负责异常处理的 catch块时,系统生成的异常对象 ex 将会传给 catch 块后的异常形参,从而允许 catch 块通过该对象来获得异常的详细信息 。
Kotlin 提供了 kotlin.Throwable 类,所有的异常类都是 Throwable 类的子类。此外,Kotlin 还通过类型别名的方式引入了 Java 的 Error和 Exception 两个子类,但 Kotlin 并没有严格区分错误和异常(在 Java 中 Error 和 Exception 的区别也不明显) 。 此外, Kotlin 完全借用了 Java 的异常体系,这些异常类之间有严格的继承关系 。
下面看 一个异常捕获的例子:
fun main(args: Array<String>) {
try {
var a = Integer.parseInt(args[0])
var b = Integer.parseInt(args[1])
val c = a / b
println(" 您输入的两个数相除的结果是 : ${c} ")
} catch (ie: IndexOutOfBoundsException) {
println(" 数组越界 : 运行程序时输入 的参数个数不够 ")
} catch (ne: NumberFormatException) {
println(" 数字格式异常 : 程序只能接收整数参数 ")
} catch (ae: ArithmeticException) {
println(" 算术异常 ")
} catch (e: Exception) {
println(" 未知异常 ")
}
}
访问异常信息
如果程序需要在 catch 块中访问异常对象的相关信息,则可以通过访问 catch 块后的异常形参来获得。当运行时环境决定调用某个 catch 块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息 。
所有的异常对象都包含了如下几个常用属性和方法。
- message: 该属性返回该异常的详细描述字符串。
- stackTrace: 该属性返回该异常的跟踪栈信息。
- printStackTrace(): 将该异常的跟踪栈信息输出到标准错误输出。
- printStackTrace(PrintStream s): 将该异常的跟踪栈信息输出到指定输出流。
try语句是表达式
与if语句类似, Kotlin 的try语句也是表达式,因此位try语句也可用于对变量赋值。 try表达式的返回值是try块中的最后一个表达式的值,或者是被执行的 catch 块中的最后一个表达式的值, finally 块中的内容不会影响表达式的结果。
如下程序示范了try语句是一个表达式的效果。
fun main(args: Array<String>) {
val input = readLine()
//用 try 表达式对变量 a 赋值
val a:Int? = try{Integer.parseInt(input)}catch (e: NumberFormatException){null}finally {
"sss"
}
println(a)
}
使用 throw抛出异常
与Java类似,Kotlin也允许程序自行抛出异常,自行抛出异常使用 throw语句来完成。
抛出异常
如果需要在程序中自行抛出异常,则应使用 throw 语句。throw 语句可以单独使用,throw 语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。 throw语句的语法格式如下:
- throw Exceptioninstance
由于 Kotlin 没有 checked 异常(即使某个异常在 Java 中原本是 checked 异常,在 Kotlin 中它也不是 checked 异常),因此 Kotlin 抛出异常的语句无须放在 try 块中,程序既可以显式使用 try...catch 来捕获井处理该异常,也可以完全不理会该异常,把该异常交给该方法调用者处理。例如下面程序:
fun main(args: Array<String>) {
//无论该异常在 Java 中是否为 checked 异常
// 在 Kotlin 中该异常都不是 checked 异常
throwChecked(-5)
throwRuntime(4)
}
fun throwChecked(a:Int){
if(a>0){
//自行抛出普通异常,在 Kotlin 中 也不 是 checked 异常
//该代码不必处于 try 块中
throw Exception("a的值大于 0,不符合要求")
}
}
fun throwRuntime(a:Int){
if(a>0){
throw RuntimeException("a的值大于 0,不符合要求")
}
}
上面代码分别抛出了 Exception 和 RuntimeException 异常, Exception 在 Java 中就是checked 异常 , 但在 Kotlin 中不是,所以我们看到上面两个方法对两种不同异常的处理完全相同。
自定义异常类
在通常情况下,程序很少会自行抛出系统异常,因为异常类的类名通常也包含了该异常的有用信息 。所以在选择抛出异常时,应该选择合适的异常类,从而可以明确地描述该异常情况。在这种情形下,应用程序常常需要抛出自定义异常 。
用户自定义异常都应该继承 Exception 基类,定义异常类时通常需要提供两个构造器: 一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的message属性的返回值) 。
下面例子程序创建了一个自定义异常类。
class AuctionException : Exception {
//无参数的构造器
constructor() {
}
//带一个字符串参数的构造器
constructor(msg: String) : super(msg) {
}
}
上面程序创建了 AuctionException 异常类 , 井为该异常类提供了两个构造器 。 尤其是带一个字符串参数的构造器,该构造器通过 super 委托调用父类的构造器, 正是这个super调用可以将此字符串参数传给异常对象的message属性, 该 message属性就是该异常对象的详细描述信息 。
在大部分情况下,创建自定义异常都可采用与 AuctionException.kt相似的代码完成,只需改变 AuctionException异常类的类名即可,让该异常类的类名可以准确描述该异常。
catch 和 throw 同时使用
实际应用的异常处理可能需要更复杂的处理方式,当一个异常出现时,单靠某个方法无法完全处理该异常,必须通过几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成 ,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。
为了实现这种通过多个方法协作处理同一个异常的情形,可以在 catch块中结合 throw语句来完成。如下例子程序示范了这种 catch 和 throw 同时使用的方法。
class AuctionTest {
var initPrice: Double = 30.0
fun bid(bidPrice: String) {
var d: Double =0.0
try {
bidPrice.toDouble()
} catch (e: Exception) {
//此处完成本方法中可以对异常执行的修复处理
//此处仅仅是在控制台打印异常的跟踪栈信息
e.printStackTrace()
//再次抛出自定义异常
throw AuctionException("”竞拍价必须是数值,不能包含其他字符!”")
}
if (initPrice > d) {
throw AuctionException("竞拍价比起拍价低,不允许竞拍! ")
}
initPrice = d
}
}
fun main(args: Array<String>) {
val at = AuctionTest()
try {
at.bid("ed")
} catch (ae: AuctionException) {
//再次捕获到 bid ()方法中的异常,并对该异常进行处理
println(ae.message)
}
}
这种 catch 和 throw 结合使用的情况在大型企业级应用中非常常用。企业级应用对异常的处理通常分成两个部分:1 后台需要通过日志来记录异常发生的详细情况:2 应用还需要根据异常向应用使用者传达某种提示。在这种情形下,所有异常都需要两个方法共同完成,也就必须结合使用 catch和 throw。
throw语句是表达式
与 try语句是表达式一样, Kotlin 的throw语句也是表达式,但由于throw表达式的类型比较特殊,是 Nothing 类型,因此很少将 throw 语句赋值给其他变量,但我们可以在Elvis表达式中使用 throw 表达式。例如如下代码:
class User(var name:String? = null,var password:String? =null){
}
fun main(args: Array<String>) {
val user =User()
//在 Elvis 表达式中使用 throw 表达式
//throw表达式表示程序出现异常,不会真正对变量赋值
val th :String = user.name ?: throw NullPointerException("”目标对象不能为 null")
println(th)
}
上面代码将user.name赋值给th变量,由于user.name 是可空类型,因此程序对 user.name使用了Elvis表达式进行判断:当 user.name不为null时,将其值赋给user.name; 否则使用 throw表达式的值 , throw表达式的类型是 Nothing, 用于表示程序无法“真正”得 到该表达式的值。因此,如果 user.name为 null,程序将会出现异常,不会对 th变量赋值, 故可将 th变量声明为String类型。
编译、运行该程序,可看到程序因为 NullPointerException 异常而结束 。
Nothing 类型没有值,而是用于标记永远无法“真正”返回的表达式,因此程序不会获取表达式的值 。 当我们自己定义函数时 ,也可使用 Nothing 来标记一个永远不会返回的函数。
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
异常的跟踪栈
异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程:
class SelfException : Exception {
constructor() {
}
constructor(msg: String) : super(msg) {
}
}
class PrintStackTraceTest{
fun firstMethod(){
secondMethod()
}
fun secondMethod(){
thirdMethod()
}
fun thirdMethod(){
throw SelfException("自定义异常信息")
}
}
fun main(args: Array<String>) {
PrintStackTraceTest().firstMethod()
}
如图: