第 11章 Java异常处理
无论程序设计的如何巧妙,一旦达到一定的复杂程度难免会出现错误。即使是简单的程序也难以避免。程序运行出错往往是没有预料到这种情况,不知道如何处理。如果是经常出错、不健壮的程序是没有人愿意使用的。甚至有的程序由于没有预料的某种错误造成对运行环境的影响,甚至造成对计算机系统的影响更是让人难以忍受。程序在运行中出现的难以预料的错误称为异常
(Exception)。 Java的异常处理机制就是尽最大努力,提供一套机制保证程序在编写中方便的处理异常,使程序在运行阶段顺利处理异常,从而使程序稳定、可靠地运行且不损伤运行系统。
本章将讲解如何合理地处理异常来编写程序、异常处理的语法结构,如何实现捕捉所有异常,以及很关键且极具威力的 finally子句。总之通过本章的学习,读者应该养成良好的编写程序的习惯,
尽力在程序的编写中处理可能出现的异常,使程序更健壮。但是异常处理也不是包治百病的良药,它仅保证程序出错的机会尽量减少,所以依然要求程序员严谨的程序设计,实现缜密的程序逻辑。
11.1 理解异常
异常指意外发生的事情。异常的发生使程序不能继续执行。
甚至在编译期程序无法完成编译,如程序员的语法错误就是一种异常事件,这种异常是 Java的默认异常,这类异常也称为编译期异常,这点将在后面讲解。当然不是所有异常在编译期都能得到处理,有的异常只有等到程序运行起来才会发现,如用户点击一个按钮,调用一个对象执行网络链接,但是由于不可预知的原因造成该对象没有初始化,
所以程序在这一点就无法继续执行,必须处理这个运行期出现的异常问题。
讲到这里读者会发现,异常涉及几个关键元素,分别是异常发生地点、异常类型、异常处理地点和异常处理函数。
异常一旦发生,程序无法继续执行下去,程序必须知道在哪个地点、什么方法或一段代码可以处理发生某种类型的异常,也就是有处理该类型异常的代码段。直到该异常得到处理。
11.2 异常示例
为了给读者一个直观的认识,在 11.1节介绍的异常概念的基础上,给出一个运行期异常的例子 。 该程序中有除 0错误,即两个整数相除,分母为 0异常,观察编译器的行为和执行该程序的结果 。 代码除 0异常示例程序所示 。
代码 除 0异常示例程序
1 public class ZeroException{
2 private int i = 10;
3 private int j = 0;
4 private int r;
5 private void getResult(){
6 r = i/j;
7 System.out.println("i/j 的计算结果是:
"+r);
8 }
9 public static void main(String[] args){
10 new ZeroException().getResult();
11 }
12 }
11.3 Java异常处理
Java是面向对象的语言,所以在 Java语言中万物皆对象、处处皆对象。在异常处理中,所谓的异常在 Java程序中就是一个异常对象。而该对象可以是系统定义好的类对象,也可以是程序员自己定义的异常类对象。总之这些异常都是对象。
在发生异常时,JVM会引发一系列行为。首先正如产生普通对象那样在 heap上创建一个异常对象,而该对象就是某个异常类的实例,
该类是 Java类库或程序员已经定义好的,每一种异常类对应一种情况的异常类型,类中可以包含该异常错误的相关信息和处理异常的方法等内容,所以对于程序抛出的异常对象总有一个异常类与之对象。一旦异常抛出,程序停止当前的执行代码,接着抛出那个异常对象的引用,异常处理代码会接手该异常对象,如果找到处理该异常类型的代码,则处理异常,否则程序将继续把该异常抛向更外层的环境去处理,如果不能处理则最终交给操作系统,
从而终止该程序的运行。
对于异常处理 Java提供了一定的语法结构。保证工作区段发生异常能够被捕获,并得到适当地处理。
11.3.1 try区块
Java的异常机制把工作代码和异常处理代码分割开,使程序结构清晰。工作代码集中于用户需要解决的问题,而异常处理代码则集中处理发生的异常事件。 Try区块就是放置可能产生异常的工作代码的区域。该区域也称为“警戒区”,意思是这里面的程序代码可能发生问题,一旦发发生问题则必须有相应的处理措施。使用 try关键字设置代码警戒区很简介,就是将工作代码放在 try关键字后一个花括号 {}内,如下所示。
1 try{
2 //可能产生异常的代码 1;
3 //可能产生异常的代码 2;
4 }
11.3.2 catch区块
catch区块是处理发生的异常 。 一旦程序发生异常则抛出该异常对象,则在异常处理函数处得到处理,这个异常处理函数就是紧跟在 try区块后的
catch区块 。 其语法格式如下所示 。
1 try{
2 Thread thread = new MyThread();
3
4 }catch(异常类型 1 对象 1){
5 //处理异常类型 1的代码
6 }
7 }catch(异常类型 2 对象 2){
8 //处理异常类型 2的代码
9 }
10 }catch(异常类型 3 对象 3){
11 //处理异常类型 3的代码
12 }
13
14 //程序中其他代码
15
11.3.3 Java异常规范
如果你设计的方法是给他人使用,并且该方法可能会抛出几种异常,此时就必须理解 Java的异常规定
( specification)。同时对于使用 Java类库中设计了异常处理的各种方法也很有帮助。方法的设计者必须让使用者知道你所设计的方法可能抛出怎样的异常类型。这样调用者就知道如何捕捉这些可能的异常。在 Java中提供了规范的语法让方法的设计者告之用户方法可能抛出的异常,这就是所谓的“异常规范”。它属于方法声明部分,在函数的参数之后,通过关键字 throws皆可能引发异常类型实现。
所以这样设计的方法如下所示。
1 private void foo() throws
Exeption1,Exception2,Exception3{
2 //函数主体
3 }
11.4 Throwable类及其子类
正如 Java的所有对象都继承自 Object一样,Java
的所有异常类都继承自类 Throwable,
该类定义了一些方法可以打印异常的描述信息,
本节将讲解 Throwable类的定义、构造及其各种获得异常信息的方法。
11.4.1 Throwable类定义和方法
类 Throwable继承自 Object,Java所有的异常类都继承 Throwable。读者可以查看 Java的 HTML文档看到该类的定义和定义的方法,这里做简要介绍。
该类的继承关系在标准文档中如图 Throwable类的继承结构所示。
11.4.2 异常类的继承关系
Java所有的异常类都继承自 Throwable,该类有两个子类,
一个是 Error,一个是 Exception。 Error类表示系统错误或编译期错误,如语法错误、函数书写错误等,一般不用。
不过这些错误我们经常遇到且可以操作的异常类是
Exception类,这类异常是 Java标准函数库中抛出的基本异常,或者是用户自定义的异常类,也可以是运行期发生的异常事件,如对象引用为 null等。
Java定义了众多的 Exception,以其子类的形式出现,这些异常对应某一种数据操作类型错误,如 IOException就是和输入输出相对应的异常,ClassNotFoundExcetion和类转载时的异常对应等。 Exception的子类在不同的 jdk版本中数量不同,但是功能模式是一样的。不同的异常类名称不同,
所捕获的异常有差异。对于每一个异常子类又有自己的子类,以处理更详细的错误。对于 Throwable子类这里不做具体的介绍,读者只要浏览一下 Java文档就一目了然了。
11.4.3 异常重抛的例子
处于某种要求或程序环境的制约,使异常需要重新抛出才可以被捕获。如 Exception异常可以捕获所有异常类型,一旦发生异常可以把该异常的引用重新抛出,使调用发生异常函数的外层环境
(调用它的函数)重新捕获该异常(该异常可能是 Exception的子类)如下代码所示。
1 catch(Exception ex){
2 ex.printStackTrace();
3 throw ex;
4 }
11.5 运行期异常
运行期异常是不需要用户“关心”的异常,此类异常 Java
会自动执行异常检查工作,出现执行期异常由系统自动抛出,记住编写 Java程序时 RuntimeException是惟一可以省略的异常。所有运行期异常都继承自 RuntimeException异常类,在编写程序时,不必考虑此类异常,所有函数都默认自己可能抛出 RuntimeException,系统会自动探测、捕获并处理运行期异常。
由于编译器不强制捕获并处理运行期异常,所以此类异常会顺利地通过编译,可以想象程序运行时刻出现
RuntimeException时,该异常会穿过层层方法,最后由系统捕获该异常,并输出相关信息。为了测试这个问题,我们设计一个例子,验证 Java对 RuntimeException到底做了什么。
11.6 自定义异常
在异常处理中 Java也提供了灵活的方式,允许自定义异常类 。 Java提供了丰富的异常类库,可以满足一般的编程需要,尤其是编译器强制捕获异常,为程序员提供了极大的方便 。 但是用户有这样的需求,即自己的程序可能产生某个特定类型的错误,而该错误是 Java类库所没有提供的,需要自定义实现 。
编写自定义异常的方法很简单,继承某个已有的 Java异常类型,而不用做任何事情 。 自定义简单异常的方法如代码自定义简单异常所示,该类首先定义一个异常类型,然后强制主类的方法 foo()抛出该类异常,一旦调用该函数就抛出 SimpleException异常 。
代码 自定义简单异常示例程序 1
1 import java.io.*;
2 //自定义异常类型 SimpleException,该类继承了 IOException
3 class SimpleException extends IOException{}
4 public class SimpleExceptionTest{
5 public void foo() throws SimpleException{
6 System.out.println("在方法 foo()内抛出 SimpleException");
7 throw new SimpleException();
8 }
9 public static void main(String[] args){
10 try{
11 new SimpleExceptionTest().foo();
12 }catch(SimpleException ex){
13 System.err.println("捕获到 SimpleException异常 ");
14 }
13 }
14}
11.7 finally子句
在程序发生异常时,在 try区块内发生异常的代码之后的程序段不能继续执行,从而跳转到 catch子句执行异常处理,但假如此时 try区块内已经执行的代码建立了网络链接,而 catch子句又没有有效关闭该链接,显然将浪费系统的资源如缓存、端口号等。所以无论 try区块是否抛出异常,都希望执行关闭数据库链接的操作,此时可以使用
finally子句执行所有异常处理函数之后的动作。
11.7.1 执行 finally子句
使用 finally子句的异常处理区段的语法如下:
Try{
//可能发生异常的代码,如建立网络链接,读数据库数据,打开文件等
//可能抛出异常类型
} catch(Type1 ex){
//处理异常类型 Type1
} catch(Type2 ex){
//处理异常类型 Type2
} catch(Type3 ex){
//处理异常类型 Type3
}finally{
//执行关键操作,考虑无论有无异常都要执行的动作,如关闭网络链接,关闭打开的文件
}
11.7.2 finally子句的必要性
本节介绍 finally子句的必要性,即该子句到底用在什么场合。直观的锁在建立了网络链接,打开数据库,打开一个磁盘文件等后的清理工作就需要 finally子句。因为在建立网络链接过程中会发生难以预料的异常类型,无论是哪个 catch子句处理触发的异常,都需要断开网络链接,释放链接资源。如果没有 finally子句该问题会变得很繁琐且很不安全。为了说明这个问题,我们设计一个开关模型,该模型设置两个具有一般意义的方法,
一个是 open()代表打开操作,一个是 shut()代表关闭打开操作所占用的系统资源。
11.8 异常的几个问题
虽然 Java提供了设计优秀的异常处理机制,但是在使用中依然存在一些瑕疵,如异常丢失问题,
而且如果异常发生在构造函数中,问题会变得更复杂,在下面的内容中,相信读者对于这些问题有深刻的体会。希望读者在使用中注意异常丢失问题,谨慎处理构造函数中的异常处理。
11.8.1 异常丢失
异常丢失是指函数抛出的异常没有被捕获,异常丢失是很严重的问题。而 Java的异常处理机制却难以弥补这个缺陷。代码提供了可能发生异常的一种情形。
该程序设计了两个自定义异常类,两个类都覆盖了 toString() 方法,toString()是对象 Java中基类
Object的方法,该方法默认的返回值是类名,这里覆盖了该方法,使其返回更直观的信息 。
11.8.2 构造函数中的异常处理
对于一般的带异常的程序而言,Java的异常处理机制通常可以保证异常被顺利处理,但是有一种情况很难处理,就是发生在类构造函数中的异常。构造函数完成对象的初始化工作,也可能在初始化时先建立网络链接(如建立
Socket通信),而断开网络链接的操作不能在构造函数内实现(就失去了构造函数的功能),需要该类另外的函数负责清理该链接,一旦构造函数内发生异常,显然对象无法建立,也就不能调用清理网络链接的方法。由此可见,
处理构造函数中的异常问题需要谨慎对待。
下面提供了一个例子,类的构造函数会首先打开一个文件来创建给类的对象实例,利用该实例分析在构造函数中处理异常的复杂性。
11.8.3 异常匹配
异常匹配讨论的是异常被抛出后,该如何选择处理函数的问题。
其实,Java提供的匹配机制很简单,异常被抛出时,系统会根据处理函数的顺序依次匹配,直到找到第一个可以处理该类异常的处理函数。系统会比较 catch子句参数的异常类型,如果该类型与抛出的异常类型相符则处理该异常,否则继续寻找。 Java异常处理机制对“异常类型相符”没有严格的要求,认为子类的异常对象与父类的异常对象相符。代码异常匹配示例程序演示了该异常匹配的过程。
11.9 异常的优点
通过本章上述的介绍读者已经理解什么是异常以及如何使用异常,在特定的情形下也可以自定义异常类。本节总结在程序编写中异常的优点。
11.9.1 分离异常处理代码
在程序中,一旦发生了异常事件,异常处理机制可以将异常处理代码与正常的程序逻辑区分开。使用一个特定的代码区块来处理异常事件,处理完毕后返回正常的程序逻辑。
在传统的编程方式中,错误的检测、报告和处理会导致复杂而冗长的代码结构。下面给出一个伪代码方式表示的示例,该代码的作用是将文件内容读入计算机的内存。
readFile{
open th file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}
11.9.2 按方法调用顺序向上传播错误
异常处理机制的第 2个优点是异常可以沿着函数的调用层次向上传播,可以在上层的任何一个可以处理该异常的函数中处理该异常。
假设 readFile方法是整个嵌套调用中的第 4个方法,而 method1()
调用 method2(),method2()调用 method3(),method3()调用
readFile()方法。如下代码所示。
Method1{
Call method2
}
Method2{
Call method3
}
Method3{
Call readFile();
}
11.9.3 分组并区分错误类型
在 Java中处处皆对象,异常也是对象。既然正常的程序逻辑中抛出的异常都是对象,那么对这些对象分类并区分这些对象是整个异常类架构内在的要求。 Java定义了一组相关的异常类,这些异常类定义在 java.io中,如
IOException和它的子类。 IOException是很普遍的异常,
如读写文件错误就属于 IOException。 IOException表示在执行 I/O操作时可能发生的任何异常对象。而它的子类则代表更具体的 I/O错误对象。如 FileNotFoundException就是无法找到文件错误。
一个方法可以编写具体的异常对象处理,在 catch子句的参数中指定具体的异常对象,如 FileNotFoundException异常,
因为该异常对象没有其他子类,所以该 catch子句只能捕获
FileNotFoundException异常。如下代码所示。
catch (FileNotFoundException e) {
//异常处理代码
}
11.10 本章习题
( 1)简答题。
1.如何理解 Java异常处理中“警戒区”的概念?
2.解释什么是 Java的异常规范?
3,Throwable类定义了一些方法。其中
printStackTrace()返回什么信息? getMessage()
和 toString()又返回什么信息?
4.在复杂的程序设计中往往需要重新抛出异常,
如果要求异常对象只记住当前的抛出点的信息,
该如何实现?
5.解释 Catch子句的执行顺序?
11.10 本章习题
( 2) 程序阅读题 。
1,下面代码是否可以正常通过编译,为什么?
try{
//工作代码
)
catch(Exception ex){
System.out.println("Caugh Exception");
}
catch(FileNotFoundException ex){
System.out.println("Caught FileNotFoundException ");
}
2,下面代码捕获什么类型的异常 。
catch(Exception ex){
//处理异常代码
}
3,下面代码是否合法 。
try{
//工作代码
)finally{
//程序的清理行为
}
11.10 本章习题
4,修改如下程序代码,使其通过编译 。
Public static void Test(String fname){
private BufferedReader in;
private String s;
try{
in = new BufferedReader(new FileReader(fname));
while((s = in.readLine()) != null)
System.out.println();}
finally{
in.close();}
}}
5,运行下面程序,体会在嵌套调用中异常处理机制的好处 。
11.10 本章习题
( 3)编程题。
1.自定义一个异常类,该类的构造接收带 String类型参数。
把该参数存储在类内部,当异常发生时,打印该参数。使用 try/catch子句测试自定义的异常类。
2.编写一段程序,Throwable类的各种方法的使用,分析
printStackTrace()的执行结果。改写该类的 toString()方法,测试 Throwable的 getMessage()方法有什么变化。
( 4)注意事项:
1.在处理异常时,只捕获非运行期异常,运行期异常由系统处理。
2.自定义异常不要继承 RuntimeException,否则会发生难以预料的问题,自定义异常要继承自 Exception异常类。
3.如果捕获异常,最好使用 finally子句释放相应的资源。
4.根据可能发生的异常类型选择异常类,根据具体情况设置处理异常的位置。