第18章 异常处理
很多事件并非总是按照人们自己设计意愿顺利发展的,而是有能够出现这样那样的异常情况。例如:你计划周末郊游,你的计划会安排满满的,你计划可能是这样的:从家里出发→到达目的→游泳→烧烤→回家。但天有不测风云,当前你准备烧烤时候天降大雨,你只能终止郊游提前回家。“天降大雨”是一种异常情况,你的计划应该考虑到这样情况,并且应该有处理这种异常的预案。
为增强程序的健壮性,计算机程序的编写也需要考虑处理这些异常情况,Kotlin语言提供了异常处理功能,本章介绍Kotlin异常处理机制。
18.1 从一个问题开始
为了学习Kotlin异常处理机制,首先看看下面程序。
//代码文件:chapter18/src/com/a51work6/section1/ch18.1.kt
package com.a51work6.section1
fun main(args: Array) {
val a = 0
println(5 / a)
}
这个程序没有编译错误,但会发生如下的运行时错误:
Exception in thread “main”
java.lang.ArithmeticException: / by zero
at com.a51work6.section1.Ch18_1Kt.main(ch18.1.kt:6)
在数学上除数不能为0,所以程序运行时表达式(5
/ a)会抛出ArithmeticException异常,ArithmeticException是数学计算异常,凡是发生数学计算错误都会抛出该异常。
程序运行过程中难免会发生异常,发生异常并不可怕,程序员应该考虑到有可能发生这些异常,编程时应该捕获并进行处理异常,不能让程序发生终止,这就是健壮的程序。
18.2 异常类继承层次
异常封装成为类Exception,此外,还有Throwable和Error类,异常类继承层次如图18-1所示。
18.2.1 Throwable类
从图18-1可见,所有的异常类都直接或间接地继承于Kotlin.lang.Throwable类,在Throwable类有几个非常重要的属性和函数:
o message属性。获得发生错误或异常的详细消息。
o printStackTrace函数。打印错误或异常堆栈跟踪信息。
o toString函数。获得错误或异常对象的描述。
为了介绍Throwable类的使用,下面修改18.1节的示例代码如下:
//代码文件:chapter18/src/com/a51work6/section2/ch18.2.1.kt
package com.a51work6.section2
fun main(args: Array) {
val a = 0
val result = divide(5, a)
println(“divide(5,
a
)
=
a) =
a)=result”)
}
fun divide(number: Int, divisor: Int): Int {
try {
return number / divisor
} catch (throwable: Throwable) { ①
println("message() : "+ throwable.message) ②
println("toString() : "+ throwable.toString()) ③
println("printStackTrace()输出信息如下:")
throwable.printStackTrace() ④
}
return 0
}
运行结果如下:
java.lang.ArithmeticException: / by zero
message() : / by zero
toString() : java.lang.ArithmeticException: / by zero
at com.a51work6.section2.Ch18_2_1Kt.divide(ch18.2.1.kt:13)
printStackTrace()输出信息如下:
at com.a51work6.section2.Ch18_2_1Kt.main(ch18.2.1.kt:6)
divide(5, 0) = 0
将可以发生异常的语句放到try-catch代码块中,称为捕获异常,有关捕获异常的相关知识会在下一节详细介绍。代码第①行是在catch中有一个Throwable对象throwable,throwable对象是系统在程序发生异常时创建,通过throwable对象可以调用Throwable中定义的函数。
代码第②行是调用message属性获得异常消息,输出结果是“/ by zero”。代码第③行是调用toString函数获得异常对象的描述,输出结果是java.lang.ArithmeticException: / by zero。代码第④行是调用printStackTrace函数打印异常堆栈跟踪信息。
18.2.2 Error和Exception
从图18-1可见,Throwable有两个直接子类:Error和Exception。
1.Error
Error是程序无法恢复的严重错误,程序员根本无能为力,只能让程序终止。例如:Java虚拟机内部错误、内存溢出和资源耗尽等严重情况。
2. Exception
Exception是程序可以恢复的异常,它是程序员所能掌控的。例如:除零异常、空指针访问、网络连接中断和读取不存在的文件等。本章所讨论的异常处理就是对Exception及其子类的异常处理。
18.3 捕获异常
从Kotlin的语法角度可以不用捕获任何的异常,因为Kotlin所有异常都运行时异常。但是捕获语句还是存在的。这一节捕获异常。
18.3.1 try-catch语句
捕获异常是通过try-catch语句实现的,最基本try-catch语句语法如下:
try{
//可能会发生异常的语句
} catch (throwable: Throwable) {
//处理异常e
}
1.try代码块
try代码块中应该包含执行过程中可能会发生异常的语句。
2. catch代码块
每个try代码块可以伴随一个或多个catch代码块,用于处理try代码块中所可能发生的多种异常。catch(throwable:
Throwable)语句中的throwable是捕获异常对象,throwable必须是Throwable的子类,异常对象throwable的作用域在该catch代码块中。
下面看看一个try-catch示例:
//代码文件:chapter18/src/com/a51work6/section3/ch18.3.1.kt
package com.a51work6.section3
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
fun main(args: Array) {
val date = readDate()
println("日期 = " + date)
}
// 解析日期
private fun readDate(): Date? { ①
try {
val str = "201A-18-18"//"201A-18-18"
val df =SimpleDateFormat("yyyy-MM-dd")
// 从字符串中解析日期
return df.parse(str) ②
} catch (e: ParseException) { ③
println("处理ParseException...")
e.printStackTrace() ④
}
return null
}
上述代码第①行定义了一个将字符串解析成日期函数,但并非所有的字符串都是有效的日期字符串,因此调用代码第②行的解析函数parse有可能发生ParseException异常,ParseException是受检查异常,在本例中使用try-catch捕获。代码第③行的e就是ParseException对象。代码第④行e.printStackTrace是打印异常堆栈跟踪信息,本例中的"2018-8-18"字符串是有个有效的日期字符串,因此不会发生异常。如果将字符串改为无效的日期字符串,如"201A-18-18",则会打印信息。
Exception in thread “main” java.text.ParseException: Unparseable date: “201A-18-18”
at java.text.DateFormat.parse(DateFormat.java:366)
at com.a51work6.section3.Ch18_3_1Kt.readDate(ch18.3.1.kt:20)
at com.a51work6.section3.Ch18_3_1Kt.main(ch18.3.1.kt:9)
18.3.2 try-catch表达式
在Kotlin中try-catch语句很多情况下使用try-catch表达式代替,Kotlin也提倡try-catch表达式写法,这样会使代码更加简洁。修改18.3.1节代码如下:
//代码文件:chapter18/src/com/a51work6/section3/ch18.3.2.kt
package com.a51work6.section3
import java.text.ParseException
import java.text.SimpleDateFormat
fun main(args: Array) {
val df =SimpleDateFormat("yyyy-MM-dd")
val date = try {// 解析日期 ①
df.parse("201A-18-18")
} catch (e: ParseException) {
null
} ②
println("日期 = " + date)
}
上述代码第①行~第②行是try-catch表达式,它相当于18.3.1节的readDate函数。
18.3.3 多catch代码块
如果try代码块中有很多语句会发生异常,而且发生的异常种类又很多。那么可以在try后面跟有多个catch代码块。多catch代码块语法如下:
try{
//可能会发生异常的语句
} catch(e : Throwable){
//处理异常e
} catch(e : Throwable){
//处理异常e
} catch(e : Throwable){
//处理异常e
}
在多个catch代码情况下,当一个catch代码块捕获到一个异常时,其他的catch代码块就不再进行匹配。
示例代码如下:
//代码文件:chapter18/src/com/a51work6/section3/ch18.3.3.kt
package com.a51work6.section3
import java.io.*
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
fun main(args: Array) {
val date = readDateFromFile()
println("读取的日期 = " + date)
}
private fun readDateFromFile(): Date? {
val df =SimpleDateFormat("yyyy-MM-dd")
try {
val fileis =FileInputStream("readme.txt") ①
val isr = InputStreamReader(fileis)
val br = BufferedReader(isr)
// 读取文件中的一行数据
val str = br.readLine() ?: return null ②
return df.parse(str) ③
} catch (e: FileNotFoundException) { ④
println("处理FileNotFoundException...")
e.printStackTrace()
} catch (e: IOException) { ⑤
println("处理IOException...")
e.printStackTrace()
} catch (e: ParseException) { ⑥
println("处理ParseException...")
e.printStackTrace()
}
return null
}
上述代码通过I/O(输入输出)流技术从文件readme.txt中读取字符串,然后解析成为日期。由于I/O技术还没有介绍,读者先不要关注I/O技术细节,这考虑调用它们的函数会发生异常就可以了。
在try代码块中第①行代码调用FileInputStream构造函数可以会发生FileNotFoundException异常。第②行代码调用BufferedReader输入流的readLine函数可以会发生IOException异常。FileNotFoundException异常是IOException异常的子类,应该先FileNotFoundException捕获,见代码第④行;后捕获IOException,见代码第⑤行。
如果将FileNotFoundException和IOException捕获顺序调换,代码如下:
try{
//可能会发生异常的语句
} catch (e: IOException) {
// IOException异常处理
} catch (e: FileNotFoundException) {
//
FileNotFoundException异常处理
}
那么第二个catch代码块永远不会进入,FileNotFoundException异常处理永远不会执行。由于上述代码第⑥行ParseException异常与IOException和FileNotFoundException异常没有父子关系,捕获ParseException异常位置可以随意放置。
18.3.4 try-catch语句嵌套
Kotlin提供的try-catch语句嵌套是可以任意嵌套,修改18.3.2节示例代码如下:
//代码文件:chapter18/src/com/a51work6/section3/ch18.3.4.kt
package com.a51work6.section3
import java.io.*
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
fun main(args: Array) {
val date = readDateFromFile()
println("读取的日期 = " + date)
}
private fun readDateFromFile(): Date? {
try {
val fileis =FileInputStream("readme.txt")
val isr = InputStreamReader(fileis)
val br = BufferedReader(isr)
try { ①
val str = br.readLine() ?:return null ②
val df =SimpleDateFormat("yyyy-MM-dd")
return df.parse(str) ③
} catch (e: ParseException) {
println("处理ParseException...")
e.printStackTrace()
} ④
} catch (e: FileNotFoundException) { ⑤
println("处理FileNotFoundException...")
e.printStackTrace()
} catch (e: IOException) { ⑥
println("处理IOException...")
e.printStackTrace()
}
return null
}
上述代码第①行~第④行是捕获ParseException异常try-catch语句,可见这个try-catch语句就是嵌套在捕获IOException和FileNotFoundException异常的try-catch语句中。
程序执行时内层如果会发生异常,首先由内层catch进行捕获,如果捕获不到,则由外层catch捕获。例如:代码第②行的readLine函数可能发生IOException异常,该异常无法被内层catch捕获,最后被代码第⑥行的外层catch捕获。
18.4 释放资源
有时在try-catch语句中会占用一些非Java虚拟机资源,如:打开文件、网络连接、打开数据库连接和使用数据结果集等,这些资源并非Kotlin资源,不能通过Java虚拟机的垃圾收集器回收,需要程序员释放。为了确保这些资源能够被释放可以使用finally代码块或自动资源管理(Automatic Resource Management)技术。
18.4.1 finally代码块
try-catch语句后面还可以跟有一个finally代码块,try-catch-finally语句语法如下:
try{
//可能会生成异常语句
} catch(e1 : Throwable){
//处理异常e1
} catch(e2 : Throwable){
//处理异常e2
} catch(eN : Throwable eN){
//处理异常eN
} finally{
//释放资源
}
无论try正常结束还是catch异常结束都会执行finally代码块,如图18-2所示。
使用finally代码块示例代码如下:
//代码文件:chapter18/src/com/a51work6/section4/ch18.4.1.kt
package com.a51work6.section4
import java.io.*
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
fun main(args: Array) {
val date = readDate()
println("读取的日期 = " + date)
}
private fun readDate(): Date? {
var fileis: FileInputStream? = null
var isr: InputStreamReader? = null
var br: BufferedReader? = null
try {
fileis =FileInputStream("readme.txt")
isr = InputStreamReader(fileis)
br = BufferedReader(isr)
// 读取文件中的一行数据
val str = br.readLine() ?: return null
val df =SimpleDateFormat("yyyy-MM-dd")
return df.parse(str)
} catch (e: FileNotFoundException) {
println("处理FileNotFoundException...")
e.printStackTrace()
} catch (e: IOException) {
println("处理IOException...")
e.printStackTrace()
} catch (e: ParseException) {
println("处理ParseException...")
e.printStackTrace()
} finally { ①
try {
if (fileis != null) {
fileis.close() ②
}
} catch (e: IOException) {
e.printStackTrace()
}
try {
if (isr != null) {
isr.close() ③
}
} catch (e: IOException) {
e.printStackTrace()
}
try {
if (br != null) {
br.close() ④
}
} catch (e: IOException) {
e.printStackTrace()
}
} ⑤
return null
}
上述代码第①行第⑤行是finally语句,在这里通过关闭流释放资源,FileInputStream、InputStreamReader和BufferedReader是三个输入流,它们都需要关闭,见代码第②行第④行通过流的close函数关闭流,但是流的close函数还有可以能发生IOException异常,所以这里又针对每一个close语句还需要进行捕获处理。
try {
…
} catch (e : FileNotFoundException) {
… …
} catch (e : IOException) {
… …
} catch (e : ParseException) {
… …
} finally {
try {
if
(readfile != null) {
readfile.close();
}
if (ir !=null) {
ir.close();
}
if (in !=null) {
in.close();
}
} catch (e : IOException){
e.printStackTrace();
}
}
18.4.2 自动资源管理
18.4.1节使用finally代码块释放资源会导致程序代码大量增加,一个finally代码块往往比正常执行的程序还要多。在Kotlin 中可以使用Java 7之后提供自动资源管理(Automatic Resource Management)技术,可以替代finally代码块,优化代码结构,提高程序可读性。
示例代码如下:
//代码文件:chapter18/src/com/a51work6/section4/ch18.4.2.kt
package com.a51work6.section4
import java.io.*
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
fun main(args: Array) {
val date = readDate()
println("读取的日期 = " + date)
}
private fun readDate(): Date? {
// 自动资源管理
try {
FileInputStream(“readme.txt”).use { fileis -> ①
InputStreamReader(fileis).use{ isr -> ②
BufferedReader(isr).use {br -> ③
// 读取文件中的一行数据
val str =br.readLine() ?: return null
val df =SimpleDateFormat("yyyy-MM-dd")
return df.parse(str)
}
}
}
} catch (e: FileNotFoundException) {
println("处理FileNotFoundException...")
e.printStackTrace()
} catch (e: IOException) {
println("处理IOException...")
e.printStackTrace()
} catch (e: ParseException) {
println("处理ParseException...")
e.printStackTrace()
}
return null
}
上述代码第①行~第③行是调用输入流use函数进行嵌套,这就是自动资源管理技术了,采用了自动资源管理后不再需要finally代码块,不需要自己close这些资源,释放过程交给了Java虚拟机。
18.5 throw与显式抛出异常
本节之前读者接触到的异常都是由于系统生成的,当异常发生时,系统会生成一个异常对象,并将其抛出。但也可以通过throw语句显式抛出异常,语法格式如下:
throw Throwable或其子类的实例
所有Throwable或其子类的实例都可以通过throw语句抛出。
显式抛出异常目的有很多,例如不想某些异常传给上层调用者,可以捕获之后重新显式抛出另外一种异常给调用者。
示例代码如下:
//代码文件:chapter18/src/com/a51work6/section5/ch18.5.1.kt
package com.a51work6.section5
class MyException : Exception { ①
constructor() { ②
}
constructor(message: String) :
super(message) {} ③
}
//代码文件:chapter18/src/com/a51work6/section5/ch18.5.kt
package com.a51work6.section5
…
fun main(args: Array) {
try {
val date = readDate()
println("读取的日期 = " + date)
} catch (e: MyException) {
println(“处理MyException…”)
e.printStackTrace()
}
}
private fun readDate(): Date? {
// 自动资源管理
try { FileInputStream("readme.txt").use { fileis ->
InputStreamReader(fileis).use{ isr ->
BufferedReader(isr).use {br ->
// 读取文件中的一行数据
val str =br.readLine() ?: return null
val df =SimpleDateFormat("yyyy-MM-dd")
return df.parse(str)
}
}
}
} catch (e: FileNotFoundException) {
throw MyException() ④
} catch (e: IOException) {
throw Throwable() ⑤
} catch (e: ParseException) {
println("处理ParseException...")
e.printStackTrace()
}
return null
}
上述代码第①行是声明了一个自定义异常,自定义异常类一般需要提供两个构造方法,一个是代码第②行的无参数的构造方法,异常描述信息是空的;另一个是代码第③行的一个字符串参数的构造方法,message是异常描述信息。
代码第④行throw MyException()语句是抛出MyException异常,代码第⑤行是抛出Throwable异常。throw显式抛出的异常与系统生成并抛出的异常,在处理方式上没有区别。
本章小结
本章介绍了Kotlin异常处理机制,其中包括Kotlin异常类继承层次、捕获异常、释放资源、throw使用。读者需要重点掌握捕获异常处理。