异常处理

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()
}

如图:


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 异常分类 Java将异常分为两种,Checked异常和Runtime异常。Java认为Checked异常都是可以在...
    LLorenzo阅读 659评论 0 1
  • 2.JAVA异常 异常指不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。异常是一个事件,它发生在程...
    青城楼主阅读 521评论 0 0
  • Java异常类型 所有异常类型都是Throwable的子类,Throwable把异常分成两个不同分支的子类Erro...
    予别她阅读 871评论 0 2
  • 初识异常(Exception) 比如我们在取数组里面的某个值得时候,经常会出现定义的取值范围超过了数组的大小,那么...
    iDaniel阅读 1,843评论 1 2
  • 记得在参加《人工智能》线下读书会时,多数书友都担心人工智能对人类的威胁,断言人类与人工智能之间必有一战。对此...
    大肚萧寒阅读 311评论 2 2