企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
## **[异常](https://www.kancloud.cn/alex_wsc/android_kotlin/1318345)处理** [TOC] ### **异常概述** 什么是异常?说到异常处理,我们想到了try、catch、finally 这样的3个关键字,是的,Kotlin里面也是这样的3个关键字,意义和Java也是一样的。我们对可能出现异常的语句使用try语句块包裹,如果try里面的语句真的出现了异常,代码将会跳转到catch语句块里面。无论代码执行顺序如何,最终都会走到finally语句块中。 Kotlin 中所有异常类都是 Throwable 类的子孙类。 每个异常都有消息、堆栈回溯信息和可选的原因。 示例 ``` fun divide(a: Int, b: Int): Int { var result: Int = a / b //定义一个result变量用于存放a/b的值 return result //将结果返回 } fun main(args: Array<String>) { var result = divide(5, 0) //调用divide()方法 println(result) } ``` 运行结果 ``` Exception in thread "main" java.lang.ArithmeticException: / by zero at FileKt.divide (File.kt:2) at FileKt.main (File.kt:7) ``` 从运行结果可知,程序在运行过程中抛出一个算术异常(ArithmeticException),这个异常是因为程序的第6行代码中,调用divide()方法时第2个参数传入了0,当程序执行第3行代码时出现了被0除的错误。出现这个异常之后,程序会立即结束,不会再执行下面的其他代码。 ArithmeticException异常类只是Kotlin异常类中的一种,在Kotlin中还提供了其他异常类,如ClassNotFoundException、ArrayIndexOutOfBoundsException、IllegalArgumentException等,这些类都继承自java.lang.Throwable类。 要抛出异常可以使用 throw -表达式来抛出异常。 ``` throw MyException("Hi There!") ``` 要捕获异常,可以使用try表达式 ``` try { // 一些代码 } catch (e: SomeException) { // 处理程序 } finally { // 可选的 finally 块 } ``` >[info]注意: ①、try表达式可以有0到多个 catch 块。 finally 块可以省略。 但是 catch 和 finally 块至少应该存在一个。 ②、在Kotlin中, throw和try都是表达式,这意味着它们可以被赋值给一个变量。这个在处理一些边界问题的时候确实非常有用 ### try…catch和finally Kotlin中提供了一种对异常进行处理的方式——**异常捕获**。异常捕获通常使用try…catch语句来实现,异常捕获的语法格式如下: ``` try { // 程序代码 }catch (e: SomeException) { // e: 后面的这个类可以是Exception 类或者其子类 // 对捕获的Exception 进行处理 } ``` 上述语法格式中,try代码块中编写的是可能发生异常的Kotlin语句,catch代码块中编写的是对捕获的异常进行处理的代码。当try代码块中的程序发生了异常,系统会将这个异常的信息封装成一个异常对象,并将这个对象传递给catch代码块。catch代码块中传递的Exception类型的参数是指定这个程序能够接收的异常类型,这个参数的类型必须是Exception类或者其子类。 * **示例** ``` fun divide(a: Int, b: Int): Int { var result: Int = a / b //定义一个result变量用于存放a/b的值 return result //将结果返回 } fun main(args: Array<String>) { try { var result: Int = divide(5, 0) println(result) } catch (e: Exception) { println("捕获的异常信息为:" + e.message) } println("程序继续执行") } ``` 运行结果 ``` 捕获的异常信息为:/ by zero 程序继续执行 ``` 在上述代码中,对可能发生异常的代码用try…catch语句进行了处理,在try代码块中发生被0除异常之后,程序会接着执行catch中的代码,通过调用Exception对象中的message来返回异常信息“by/zero”,catch代码块对异常处理完毕之后,程序会继续向下执行,而不会出现异常终止。 >[info]需要注意的是,在try代码块中,发生异常的语句后面的代码是不会被执行的,如本案例中的第8行代码的输出语句就没有执行。 在程序中,由于功能的需求,有些语句在无论程序是否发生异常的情况下都要执行,这时就可以在try…catch语句后添加一个finally代码块。接下来我们来对上面的代码进行修改,看一下finally代码块的用法,具体代码如下所示。 ``` fun divide(a: Int, b: Int): Int { var result: Int = a / b //定义一个result变量用于存放a/b的值 return result //将结果返回 } fun main(args: Array<String>) { try { var result: Int = divide(5, 0) //调用divide()方法 println(result) } catch (e: Exception) { println("捕获的异常信息为:" + e.message) return //用于结束当前语句 } finally { println("进入finally代码块") } println("程序继续执行") } ``` 运行结果 ``` 捕获的异常信息为:/ by zero 进入finally代码块 ``` 在上述代码中,catch代码块中增加了一个return语句,用于结束当前方法,此时第15行的代码`println("程序继续执行")`就不会执行了,而finally中的代码仍会执行,并不会被return语句所影响。 不论程序发生异常还是用return语句结束当前方法,finally中的语句都会执行,由于finally语句的这种特殊性,因此在程序设计时,经常会在try…catch后使用finally代码块来完成必须做的事情,如释放系统资源。 >[info]需要注意的是,当catch代码块中执行了System.exit(0)语句时,finally中的代码块就不会执行。System.exit(0)表示退出当前的程序,退出当前程序后,任何代码都不能再执行了。 #### 与Java中的try…catch…finally对比 与Java不同的是,**在try…catch…finally中,try是一个表达式,即它可以返回一个值,try表达式的返回值是try代码块中的最后一个表达式的结果或者是所有catch代码块中的最后一个表达式的结果,finally代码块中的内容不会影响表达式的结果**。接下来我们通过一个案例来演示try…catch…finally代码块中的结果,代码如下所示 ``` fun main(args: Array<String>) { var age: Int? = try { 10 //正常运行,返回10 } catch (e: NumberFormatException) { 12 null } finally { 13 //finally代码块不会影响表达式结果 } var score: Int? = try { Integer.parseInt("s" + 1) } catch (e: NumberFormatException) { 60 null //捕获异常,返回null } finally { 70 //finally代码块不会影响表达式结果 } println("年龄age=${age}") println("分数score=${score}") } ``` 运行结果 ``` 年龄age=10 分数score=null ``` 上述代码中,分别定义了2个变量age与score,age表示年龄,score表示分数,这2个变量的值都是用try…catch…finally代码块包括起来的,根据程序的运行结果可知,当给变量age赋值时,程序正常运行,则age的值是10,即try表达式的结果。当给变量score赋值时,程序出现了异常(s不知从何而来,Integer是Java中的类,字符串默认值是null,即为空未分配对象,看Integer源码就知道,Integer.parseInt方法参数为null时,`throw new NumberFormatException("null")`),此时score的值是null,即catch代码块中最后一个表达式的结果。其中finally代码块中的内容不影响try和catch代码块中表达式的结果。 总结: 在try…catch…finally语句中,catch代码块可以有多个也可以没有,finally代码块可以省略,但是catch和finally代码块至少应有一个是存在的。 #### **Try 是⼀个表达式,可以有一个返回值** 【Kotlin相比于Java,可以使用变量try表达式返回值】。try表达式要么有try语句块的最后一行决定,要么由catch语句块的最后一行决定;finally代码段的内容不会影响try表达式的结果值。参考如下代码: ![](https://box.kancloud.cn/d641eb1949151aad0704404c1f337dcc_984x627.png) 针对以上代码,如果try语句块没有异常,返回的就是字符串转换转换的结果,如果出现异常,就会走到catch语句块,返回的就是0。 运行结果: ![](https://box.kancloud.cn/e5d4ef9fa7248b21126d2a96df34c5d6_979x123.png) ### **throw表达式** 前面的代码中,由于调用的是自己写的divide()方法,因此很清楚该方法可能会发生异常。试想一下,如果去调用一个别人写的方法时,是否能知道别人写的方法是否会有异常呢?这是很难做出判断的。针对这种情况,在Kotlin中,允许在可能发生异常的代码中通过throw关键字对外声明该段代码可能会发生异常,这样在使用这段代码时,就明确地知道该段代码有异常,并且必须对异常进行处理。 throw关键字抛出异常的语法格式如下: ``` throw ExceptionType("异常信息") ``` 上述语法格式是通过throw表达式来抛出一个异常,这个表达式需要放在容易出现异常的代码中,也可以作为Elvis表达式(三元表达式)的一部分,throw后面需要声明发生异常的类型,通常将这种做法称为抛出一个异常。接下来我们来修改前面的代码,在调用divide()方法的代码上方抛出一个异常,修改后的代码如下所示 ``` fun divide(a: Int, b: Int): Int { if (b==0)throw ArithmeticException("发生异常") var result: Int = a / b return result } fun main(args: Array<String>) { var result: Int = divide(5, 0) //调用divide()方法 println(result) } ``` 报错如下 ``` Exception in thread "main" java.lang.ArithmeticException: 发生异常 at FileKt.divide (File.kt:2) at FileKt.main (File.kt:7) ``` 上述代码中,第7行代码调用divide()方法时传递的第2个参数是0,程序在运行时不会发生被0除的异常,由于在divide()方法中抛出了一个算术异常ArithmeticException,因此在调用divide()方法时就必须对抛出的异常进行处理,否则就会发生编译错误。接下来我们对代码进行修改,在调用divide()方法时对其进行try…catch处理,修改后的代码如下所示。 ``` fun divide(a: Int, b: Int): Int { if (b==0)throw ArithmeticException("发生异常") var result: Int = a / b return result } fun main(args: Array<String>) { try { var result: Int = divide (5, 0) //调用divide()方法 println(result) } catch (e: ArithmeticException) { println(e.message) } } ``` 运行结果 ``` 发生异常 ``` 上述代码中,在main()函数中通过try…catch语句已经捕获了divide()方法中抛出的异常,由于该程序的divide()方法中传递的变量b的值为0,因此运行结果为发生异常。如果将变量b的值设置为1,则运行结果为5。 #### 用于Elvis 表达式的⼀部分 在 Kotlin 中 throw 是表达式,所以你可以使⽤它(⽐如)作为 Elvis 表达式的⼀部分: ``` val s = person.name ?: throw IllegalArgumentException("Name required") ``` #### Nothing类型 参考后面的章节——[kotlin.Nothing类型](https://www.kancloud.cn/alex_wsc/android_kotlin/1053641)和官方文档[Nothing 类型](http://www.kotlincn.net/docs/reference/exceptions.html#nothing-%E7%B1%BB%E5%9E%8B) 在 Kotlin 中 throw 是表达式,它的类型是特殊类型 Nothing。 该类型没有值。跟C、Java中的 void 意思一样,⽽是⽤于标记永远不能达到的代码位置。 在Kotlin中,有些函数的“返回值类型”的概念没有任何意义,此时Kotlin使用一种特殊的返回值类型Nothing来表示,Nothing是一个空类型(uninhabited type),也就是说在程序运行时不会出任何一个Nothing类型的对象。在程序中,可以使用Nothing类型来标记一个永远不会有返回数据的函数, 我们在代码中,用 Nothing 来标记无返回的函数: ~~~ fun main(args: Array<String>) { fail("Error occurred") } fun fail(message:String):Nothing{ throw IllegalStateException(message) } ~~~ 当调用含有Nothing类型的函数时,可能会遇到这个类型的一种情况就是类型推断,这个类型的可空类型为Nothing?,该类型有一个可能的值是null,如果用null来初始化一个变量的值,并且又不能确定该变量的具体类型时,编译器会推断这个变量的类型为Nothing?类型,具体示例代码如下: ``` val x = null // 变量“x”的类型为“Nothing? ” val l = listOf(null) // 变量“l”的类型为“List<Nothing?>” ``` 上述代码中,变量x的初始值为null,并且该变量的具体类型不确定,因此编译器会推断该变量的类型为“Nothing?”。变量l的初始值为listOf(null),可以看到List集合的初始值为null,此时不能确定该集合的具体类型,因此编译器会推断该集合的类型为“List<Nothing?>”。 * 另外,如果把一个throw表达式的值赋值给一个变量,需要显式声明类型为 Nothing , ``` val ex:Nothing = throw Exception("YYYYYYYY") ``` >[info]【注意】另外,因为ex变量是Nothing类型,没有任何值,所以无法当做参数传给函数 可能会遇到这个类型的另⼀种情况是类型推断。这个类型的可空变体 Nothing? 有⼀个可能的值是 null 。如果⽤ null 来初始化⼀个要推断类型的值,⽽⼜没有其他信息可⽤于确定更具体的类型时,编译器会推断出 Nothing? 类型: ``` val x = null // “x”具有类型 `Nothing?` val l = listOf(null) // “l”具有类型 `List<Nothing?> ``` ### **没有受检异常** **Kotlin没有受检异常(Checked Exceptions)**。 Java里面有两种异常类型,一种是受检异常(checked exception),一种是非受检异常(uncheckedexception)。 之所以编写Java代码的时候,IDE会提示进行try catch操作,因为编译时编译器会检查受检异常。 受检异常(Checked Exceptions)显得比较麻烦,一直以来争议比较大,可能会导致java API变得复杂,程序员编写代码的时候需要进行大量的try catch操作。所以,【Kotlin相比于Java,没有了受检异常,IDE不会提示进行try catch操作】。 写代码的时候,IDE调用某一个方法,这个方法即使可能抛出异常,IDE也不会提示你进行`try…catch`操作,直接运行该程序时会抛出异常信息。参考如下代码: ![](https://box.kancloud.cn/b6267b3d90998baa9b7de986f725b276_984x297.png) 针对以上代码,如果是java代码,【Integer.parseInt(line)】,这样的代码是会提示我们进行trycatch操作的,但是Kotlin不会提示。如果直接运行会抛出转换异常,参考截图: ![](https://box.kancloud.cn/f6b962f8694f98d2c0e3e48726494460_972x121.png) 我们可以自己给它加上try catch操作,参考如下代码: ![](https://box.kancloud.cn/cf18dc1b6a25d0297d18a3dcdac5887b_986x766.png) * **JDK举例** 以下是 JDK 中 StringBuilder 类实现的⼀个⽰例接⼝: ``` Appendable append(CharSequence csq) throws IOException; ``` 这个签名是什么意思? 它是说,每次我追加⼀个字符串到⼀些东西(⼀个 StringBuilder 、某种⽇志、⼀个控制台等)上时我就必须捕获那些IOException 。 为什么?因为它可能正在执⾏ IO 操作( Writer 也实现了 Appendable )…… 所以它导致这种代码随处可⻅的出现: ``` try { log.append(message) } catch (IOException e) { // 必须要安全 } ``` **结论**:通过一些小程序测试得出的结论是异常规范会同时提高开发者的生产力与代码质量,但是大型软件项目的经验表明一个不同的结论——生产力降低、代码质量很少或没有提高。 ### 自定义异常 在Kotlin标准库中封装了大量的异常类,虽然这些异常类可以描述编程时出现的大部分异常情况,但是在程序开发中有时可能需要描述程序中特有的异常情况,例如在设计一个变量name的值时,不允许该变量的值为null,为了解决这个问题,Kotlin与Java一样,也允许用户自定义异常,但**自定义的异常类必须继承自Throwable类或其子类**,自定义异常类的主构函数可以传递一个String类型的message参数,也可以不传递参数,自定义异常类的语法格式如下: ``` //主构函数带参数的异常类 class异常类名称(override val message: String?) : Throwable() {} //主构函数不带参数的异常类 class异常类名称() : Throwable() {} ``` 在上述语法格式中,主构造函数带message参数的异常类,在抛出异常信息时会打印message信息,主构造函数不带message参数的异常类,在抛出异常信息时不会打印异常的具体信息。 示例 ``` class MyException(override val message: String?) : Throwable() { } fun main(args: Array<String>) { var name: String? = null name?.length ?: throw MyException("变量name的值为null") println("name.length=${name.length}") } ``` 报错如下 ``` Exception in thread "main" MyException: 变量name的值为null at FileKt.main (File.kt:5) ``` 从运行结果可以看出,程序在运行时发生了异常,这是因为在程序中使用throw关键字抛出异常对象时,需要使用`try…catch`语句对抛出的异常进行处理。为了解决该程序运行时发生异常的问题,可以修改该程序,在抛出异常的地方使用`try…catch`语句对异常进行处理,修改后的代码如下所示。 ``` class MyException(override val message: String?) : Throwable() { } fun main(args: Array<String>) { var name: String? = null try { name?.length ?: throw MyException("变量name的值为null") println("name.length=${name.length}") } catch (e: MyException) { println(e.message) } } ``` 运行结果 ``` 变量name的值为null ``` 在上述代码中,使用了一个try…catch语句用于捕获变量name的值为null时抛出的异常,在调用“name?.length”代码时,由于变量name的值不能为null,程序会抛出一个自定义异常MyException,该异常被捕获后最终被catch代码块处理,并打印异常信息。