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() 方法可以很方便地用于跟踪异常的发生情况,可以调试程序,但在最后发布的程序中,应该避免使用它

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

推荐阅读更多精彩内容

  • 异常是什么 异常就是有异于常态,和正常情况不一样,有错误出现。 异常分类 Error: 是程序中无法处理的错误,表...
    Method阅读 2,084评论 0 0
  • try-catch 语句try-catch 表达式多 catch 代码块try-catch 语句嵌套 一、try-...
    狼性代码人阅读 4,131评论 0 2
  • 异常在学习与开发当中是最常见的,Kotlin的异常处理和Java以及其他开发语言的处理方式很相似。一个函数可以正常...
    阿博聊编程阅读 1,385评论 0 1
  • 欢迎前往我的csdn kotlin 异常处理机制类似 Java 异常处理机制。但有一点不同,Java 中的异常分为...
    AlfredZSGao阅读 120评论 0 1
  • finally 代码块自动资源管理   有时在 try-catch 语句中会占用一些非 Java 虚拟机资源,如打...
    狼性代码人阅读 1,573评论 0 2