kotlin异常处理

异常处理

Kotlin 的异常处理机制主要依赖于try、catch、finally、throw 这四个关键字。

  1. try 块里面放置可能引发异常的代码
  2. catch 后对应异常类型和一个代码块,表明该 catch 块用于处理这种类型的代码块
  3. finally 块,用于回收在 try 块里打开的物理资源,异常处理机制会保证 finally 块总被执行
  4. 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() 方法可以很方便地用于跟踪异常的发生情况,可以调试程序,但在最后发布的程序中,应该避免使用它

你可能感兴趣的:(kotlin异常处理)