欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《Kotlin从小白到大牛》第18章:异常处理

程序员文章站 2022-07-14 19:42:00
...

第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所示。
《Kotlin从小白到大牛》第18章:异常处理
18.2.1 Throwable类
从图18-1可见,所有的异常类都直接或间接地继承于Kotlin.lang.Throwable类,在Throwable类有几个非常重要的属性和函数:
o message属性。获得发生错误或异常的详细消息。
o printStackTrace函数。打印错误或异常堆栈跟踪信息。
o toString函数。获得错误或异常对象的描述。
《Kotlin从小白到大牛》第18章:异常处理
为了介绍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) =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函数打印异常堆栈跟踪信息。
《Kotlin从小白到大牛》第18章:异常处理
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)
《Kotlin从小白到大牛》第18章:异常处理
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代码块就不再进行匹配。
《Kotlin从小白到大牛》第18章:异常处理
示例代码如下:
//代码文件: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捕获。
《Kotlin从小白到大牛》第18章:异常处理

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所示。
《Kotlin从小白到大牛》第18章:异常处理
使用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语句还需要进行捕获处理。
《Kotlin从小白到大牛》第18章:异常处理
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虚拟机。
《Kotlin从小白到大牛》第18章:异常处理

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从小白到大牛》第18章:异常处理

本章小结

本章介绍了Kotlin异常处理机制,其中包括Kotlin异常类继承层次、捕获异常、释放资源、throw使用。读者需要重点掌握捕获异常处理。