第九章 Java异常处理
提要:
掌握Java的异常处理机制
使用try、catch、finally处理异常
使用throw和throws引发异常
getMessage和printStackTrace方法
自定义异常类
异常处理是程序设计中一个非常重要的方面,也是程序设计的一大难点。Java语言在设计的当初就考虑到这些问题,提出异常处理的框架的方案,所有的异常都可以用一个类型来表示,不同类型的异常对应于不同的子类异常,定义异常处理的规范。在1.4版本以后增加了异常链机制,从而便于跟踪异常。这是Java语言设计者的高明之处,也是Java语言学习中一个重要的知识点。Java的异常处理机制可以使程序设计人员方便、快捷地处理程序执行过程中出现的各种异常情况,在很大程度上提高了程序编写和测试的效率。
9.1 异 常 概 述
在程序设计中,错误通常分为两类,即编译错误和运行错误。编译错误是比较容易发现的,而运行错误常常让开发人员感到头疼。异常就是一个运行错误,如果不能很好地处理异常,则项目的稳定性就不强。所以为了增强项目的稳健性,就要求对出现异常时进行相关处理,亦即异常处理。
其实在生活中,也可以很容易找到诸如异常的事例。例如,小王每天开车去上班,正常情况下耗时在30分钟以内,若把上班的动作描述为方法,则伪代码为:
上班(){
输出 一路畅通;
}
用示意图来描述上班这个方法效果如图9.1所示。

图9.1 示意图(1)
如果遇上道路限行或其他交通情况就会耗时大于30分钟,对于这种情况,生活中肯定会及时与工作单位取得联系,通过电话请假避免影响工作。而对于程序而言,这就算是一种异常情况。及时与工作单位取得联系的做法可用编程术语描述为“异常处理”。其示意图如图9.2所示。

图9.2 示意图(2)
在不支持异常处理的程序设计语言中,程序员为了检查可能发生的异常情况,需要在程序中设置一些标记量,并使用很多的if…else语句,并且要求程序员非常清楚地知道是什么导致了异常的产生以及异常的确切含义。若用代码来描述图9.2所示的过程,则伪代码为:
上班(){
设置标量值;
调用标量输出(标量值);
}
标量输出(标量值){
if(堵车){
call公司;
输出 堵车;
}else if(撞车){
call 公司撞车;
输出 撞车;
}else{
输出 一路畅通;
}
}
而在Java中,没有必要去编写上述的这些if…else语句,在默认的情况下,异常会输出一个错误消息,并中止线程的执行。为了更好地处理异常情况,程序员通常会在程序中定义异常处理段来捕获和处理异常。这样,当异常情况发生时,一个代表该异常类的对象就会被创建,并在产生异常的方法中被触发。上班()方法可以选择处理异常的方式:由自己处理或抛出该异常。使用Java语言提供对异常处理的支持,改写上班的伪代码如下所示。
上班()可能存在 堵车,撞车{
try{
输出 一路畅通;
}catch(堵车){
call 公司堵车
} catch(撞车){
call 公司撞车;
}
}
通过比较可以发现,采用异常类不仅可以使代码变得更加简洁,而且能够为程序调试提供很大的方便,从而达到提高程序健壮性的目的。
Java提供了一个Throwable类,它的API界面截图如图9.3所示。

图9.3 Throwable类的API界面截图
Throwable类是Java语言中所有错误或异常的超类。只有当对象是此类(或其子类之一)的实例时,才能通过Java虚拟机或者Java的throw语句抛出。类似地,只有此类或其子类之一才可以是catch子句中的参数类型。它的两个子类的实例Error 和 Exception通常用于指示发生了异常情况。这些实例是在异常情况的上下文中新近创建的,因此包含了相关的信息(比如堆栈跟踪数据)。Throwable 类及其子类的结构如图9.4所示。

图9.4 Throwable类及其子类结构图
在Java 编程语言中,异常有两种分类。java.lang.Throwable类充当所有对象的父类,可以使用异常处理机制将这些对象抛出并捕获。在Throwable类中定义方法来检索与异常相关的错误信息,并打印显示异常发生的栈跟踪信息。它有Error 和Exception 两个基本子类。
错误(Error):JVM系统内部错误、资源耗尽等严重情况;
异常(Exception 违例):其它因编程错误或偶然的外在因素导致的一般性问题,例如:对负数开平方根、空指针访问、试图读取不存在的文件、网络连接中断等。
当发生Error 时,程序员根本无能为力,只能让程序终止。比如说内存溢出,不可能指望程序能处理这样的情况。而对于Exception,而有补救或控制的可能,程序员也可以预先防范,本章主要讨论Exception 的处理。Exception类的API界面截图如图9.5所示。

图9.5 Exception类的API界面截图
图9.5中包括了众多Exception类的已知子类,读者不必担心,也没有必要把每个子类都搞清楚,在实际使用时再查阅异常类的文档即可。下面列举一些常见的异常类,如表9.1所示。
表9.1 常见异常类
|
异 常 |
说 明 |
|
Exception |
异常层次结构的根类 |
|
RuntimeException |
许多java.lang异常的基类 |
|
ArithmeticException |
算术错误情形,如以零作除数 |
|
IllegalArgumentException |
方法接收到非法参数 |
|
ArrayIndexOutOfBoundException |
数组大小小于或大于实际的数组大小 |
|
NullPointerException |
尝试访问null对象成员 |
|
ClassNotFoundException |
不能加载所需的类 |
|
NumberFormatException |
数字转化格式异常,比如字符串到float型数字的转换无效 |
|
IOException |
I/O异常的根类 |
|
FileNotFoundException |
找不到文件 |
|
EOFException |
文件结束 |
|
InterruptedException |
线程中断 |
从编程角度考虑还可以将异常分为以下几种。
1. 非受检异常
非受检(unchecked)异常是指编译器不要求强制处置的异常。一般是指因设计或实现方式不当导致的问题。也可以说,是程序员的原因导致的,本来可以避免发生的情况。java.lang.RuntimeException类及它的子类都是非受检异常,具体如下。
l 错误的类型转换异常:java.lang.ClassCastException。
l 组下标越界异常:java.lang.ArrayIndexOutOfBoundsException。
l 空指针访问异常:java.lang.NullPointerException。
l 除零溢出异常:java.lang.ArithmeticException。
如果事先检查数组元素下标保证其不超出数组长度,ArrayIndexOutOfBoundsException 异常从不会抛出;再如,先检查并确保一个引用类型变量值不为null,然后在令其访问所需的属性和方法,那么,NullPointerException 也就从不会产生。如果程序设计良好并且正确实现,这类异常便永远不会发生,所以通常也不会处理这类异常。
2. 受检异常
受检(checked)异常是指编译器要求必须处置的异常,即程序在运行时由于外界因素造成的一般性异常,具体如下。
l 没有找到具有指定名称的类异常:java.lang.ClassNotFoundException。
l 访问不存在的文件异常:java.io.FileNotFoundException。
l 操作文件时发生的异常:java.io.IOException。
l 操作数据库时发生的异常:java.sql.SQLException。
Java编译器要求Java程序必须捕获或声明所有受检异常,如:FileNotFoundException、IOException等。因为,对于这类异常来说,如果程序不进行处理,可能会带来意想不到的结果。而非受检异常可以不作处理,因为这类异常很普遍,若全部处理可能会对程序的可读性和运行效率产生影响。
9.2 使用try和catch捕获异常
Java程序在执行过程中如果出现异常,会自动生成一个异常对象,该异常对象将被自动提交给JVM,当JVM接收到异常对象时,会寻找能处理这一异常的代码并把当前异常对象交给其处理,这一过程称为捕获(catch)异常。如果JVM找不到可以捕获异常的方法,则运行时系统将终止,相应的Java程序也将退出。
在Java中捕获异常时,首先用try选定要捕获异常的范围,在执行时,Catch后面括号内的代码会产生异常对象并被抛出,然后就可以用catch块来处理异常了。try和catch应用效果图如图9.6所示。

图9.6 try和catch应用效果图
try的意思就是测试它所包含的代码是否会发生异常,而catch的意思就是在异常发生时就抓住它,并进行相应的处理,使程序不受该异常的影响从而继续执行下去。它们通常使用的格式如下。
try {
// 代码段(可能发生异常代码)
} catch (异常类型 ex) {
// 对异常进行处理的代码段
}
// 代码段
【示例1】TryDemo.java
通过示例TryDemo.java演示try和catch的用法。
public class TryDemo {
/**
* @param args
*/
public static void main(String[] args) {
int number=0;
try{
number=Integer.parseInt(args[0]);
//如果存在异常,下面这一行代码是不会输出的
System.out.println("看得见吗?");
}catch(Exception e){
System.out.println("非法的数字");
}
System.out.println("你输入的数字为:"+number);
}
}
TryDemo.java描述了从控制台获取用户输入的参数,如果将参数转换为数字成功,就输出转换后的数字;如果转换有异常,就说明用户输入的是非法的数字。代码比较简单,主要是通过这段代码让读者思考两个问题:一是程序没有异常时catch中的语句是否会被执行?二是程序中有异常时最后的语句还会被执行吗?
带着疑问,笔者通过实际操作找寻答案。
1. 输入一个未能转化为数字的参数
在菜单栏中选择Run→Open as Dialog命令,在弹出的Run对话框中选择Arguments标签,在Program arguments文本框中输入“hello”,然后单击Run按钮,如图9.7所示。

图9.7 设置main方法的参数
控制台的输出结果如图9.8所示。

图9.8 当参数不为数字时的输出结果
2. 输入一个能转化为数字的参数
重复如图9.7所示的操作,在Program arguments文本框中输入“2”,然后单击Run按钮。控制台的输出结果如图10.9所示。

图10.9 当参数为数字2时的输出结果
通过这两种方式的比较可以看出,当有异常发生时,要执行catch后面括号中的语句,并且catch之后的语句也会执行;没有异常发生时,不会执行catch后面括号中的语句。
9.3 使用throw和throws引发异常
前面讨论了如何捕获Java运行时由系统引发的异常,如果想在程序中明确地引发异常,则需要用到throw或throws语句。
1. throw语句
throw语句用来明确地抛出一个“异常”。这里请注意,用户必须得到一个Throwable类或其他子类产生的实例句柄,通过参数传到catch子句,或者用new语句来创建一个实例。throw语句的通常形式如下。
throw ThrowableInstance
【示例2】ThrowDemo.java
创建ThrowDemo.java演示throw的相关用法。
ThrowDemo.java的代码清单如下所示。
public class ThrowDemo {
/**
* @param args
*/
public static void main(String[] args) {
int number=0;
try{
number=Integer.parseInt(args[0]);
}catch(Exception e){
throw new ArrayIndexOutOfBoundsException("数组越界");
//System.out.println("非法的数字");
}
System.out.println("你输入的数字为:"+number);
}
}
若不提供参数,会引发数组越界的异常(ArrayIndexOutOfBoundsException即为数组越界异常类,如果想了解异常的详细情况,请参见9.5小节内容)。运行ThrowDemo类,控制台的输出结果如图9.10所示。
执行throw语句后,运行流程将立即停止,throw的下一条语句也将暂停执行。throw后必须抛出一个Throwable类的实例。

图9.10 当参数为空时的输出结果
从图9.10可以看出,throw语句一经执行,后面的语句都不会被执行。
2. throws语句
如果一个方法a可以引发异常,而它本身并不对该异常进行处理,那么a方法必须将这个异常抛给调用以使程序能够继续执行下去。这时候就要用到throws语句。throws语句的常用格式如下所示。
returnType methodName() throws ExceptionType1,ExceptionType2,…{
… //method body
}
在method body中可以是引发异常列表中的任何一种异常及其子类型的异常。thorws用来声明一个方法可能会抛出的所有异常,它跟在方法签名的后面。如果有多个异常则使用逗号将其分开。如果一个方法声明的是受检异常,则在调用这个方法之处必须处理这个异常(一般情况下由调用此方法的类处理)。
【示例3】ThrowsDemo.java
创建ThrowsDemo.java演示throws的相关用法。
public class ThrowsDemo {
/**
* @param args
*/
public static void main(String[] args) {
testThrows(args);
}
public static void testThrows(String[] tmp) {
try {
createThrow(tmp);
} catch (Exception e) {
System.out.println("来自createThrow方法的异常");
}
}
// 抛出可能存在的异常
public static void createThrow(String[] tmp) throws Exception {
int number = 0;
number = Integer.parseInt(tmp[0]);
System.out.println("你输入的数字为:" + number);
}
}
如果方法签名后有throws关键词,则在此方法被调用时,需要在调用方法中用try和catch进行捕获异常,如果不捕获异常,则需要在调用方法中使用throws关键词将异常抛出。代码中的testThrows()方法也可以改为如下所示。
public static void testThrows(String[] tmp) throws Exception {
createThrow(tmp);
}
被调用的方法和调用方法处理异常的关系示意图如图9.11所示。

图9.11 被调用的方法和调用方法处理异常的关系示意图
当覆盖抛出异常的方法时,覆盖方法仅需要声明异常的同类或子类,例如,如果父类方法抛出IOException,则覆盖方法可以抛出IOException、FileNotFoundException(IOException的子类),但不可以抛出Exception(IOException的父类)。可以在throws子句中声明更少或更多指定的异常。
3. throw和throws语句的组合应用
在实际的应用中,一般都需要throw和throws语句组合应用,就是在捕获异常后,抛出一个明确的异常给调用者。
【示例4】ThrowAndThrowsDerno.java
创建ThrowAndThrowsDerno.java演示throw和throws语句的组合应用。
public class ThrowAndThrowsDemo {
/**
* @param args
*/
public static void main(String[] args) {
testThrow(args);
}
// 调用有异常的方法
public static void testThrow(String[] tmp) {
try {
createThrow(tmp);
} catch (Exception e) {
System.out.println("捕捉来自createThrow方法的异常");
}
}
// 抛出一个具体的异常
public static void createThrow(String[] tmp) throws Exception{
int number = 0;
try {
number = Integer.parseInt(tmp[0]);
} catch (Exception e) {
throw new ArrayIndexOutOfBoundsException("数组越界");
// System.out.println("非法的数字");
}
System.out.println("你输入的数字为:" + number);
}
}
throw语句是编写在方法之中的,而throws语句是用在方法签名之后的。在同一个方法中使用throw和throws时要注意,throws抛出的类型的范围要比throw抛出的对象范围大才可以。
9.4 finally关键字
前面讲过,如果try语句块中存在异常,则这行产生异常代码之后的语句将不再被执行。但在某些特定的情况下,不管是否有异常发生,总是要求某些特定的代码必须被执行,比如在讲解与数据库连接时,不管对数据库的操作是否成功,最后都需要关闭数据库的连接以释放内存资源。这就需要用到finally关键字。finally用法示意图如图9.12所示。

图9.12 finally用法示意图
finally的使用格式如下所示。
try {
… // 代码段(可能发生异常代码)
} catch (异常类型 ex) {
… // 对异常进行处理的代码段
}finally{
… //总要被执行的代码
}
… // 代码段
【示例5】FinallyDemo.java
下面通过FinallyDemo.java示例演示finally的用法。
public class FinallyDemo {
public static void main(String[] args) {
System.out.println("请打开数据库连接......");
try {
System.out.println("执行查询操作");
System.out.println("执行修改操作");
int i = 12 / 0;
System.out.println("执行添加操作");
System.out.println("执行删除操作");
} catch (Exception ex) {
System.out.println("除零出错!");
ex.printStackTrace();
} finally {
System.out.println("请关闭数据库连接......");
}
}
}
这里为了演示finally关键字的效果,特意设置了除零的应用来制作异常。运行FinallyDemo.java,控制台的显示结果如图9.13所示。

图9.13 FinallyDemo.java示例运行结果图
到这里Java的异常处理所信赖的五个关键字都讲述完毕了,这五个关键字:try、catch、 finally、throw、throws 的关系如图9.14所示。

图9.14 关键字try、catch、finally、throw、throws的关系示意图
通过MyEclipse debug的调试技术可以查看各种程序的异常,常用的调试技巧有:设置断点,遇到断点,暂挂,等候命令;选择debug as→Java Application命令;运用以下快捷键。
F5:单步跳入。进入本行代码中执行。
F6:单步跳过。执行本行代码,跳到下一行。
F7:单步返回。跳出方法。
F8:继续。执行到下一个断点,如果没有断点了,就执行到结束。
Ctrl+R:执行到光标所在的这一行。
还要强调的一点是:只有一种情况会阻止finally子句执行,就是虚拟机被关闭。不管try是以何种方式结束的(正常结束、异常结束、通过return或break控制流语句结束),finally子句也总是恰好在成员函数返回前执行。
通过示例ReturnExceptionDemo.java讲解Return语句的作用。
【示例6】ReturnExceptionDemo.java
public class ReturnExceptionDemo {
static void methodA() {
try {
System.out.println("进入方法A");
throw new RuntimeException("制造异常");
// 抛出异常
} finally {
System.out.println("用A方法的finally");
// 注意:不管发生什么都会执行!!
}
}
static void methodB() {
try {
System.out.println("进入方法B");
return;
// 返回,实际上是在finally语句执行完后才返回
} finally {
System.out.println("调用B方法的finally");
}
}
public static void main(String[] args) {
try {
methodA();
} catch (Exception e) {
System.out.println(e.getMessage());
}
methodB();
}
}
运行此程序,输出结果如图9.15所示。

图9.15 ReturnExceptionDemo.java的输出结果
finally 语法最早是在微软的 SEH 所设计出的一种机制,虽然它功能很强大,但是实现起来却并不是很难,从表象可以这样来理解:当代码在执行过程中,遭遇到 return 和 goto 等类似的语句所引发作用域(代码执行流)转移时,便会产生一个局部展开(Local Unwinding);而由于异常而导致的 finally 块被执行的过程,往往被称为全局展开(Global Unwinding)。由于展开(Unwinding)而导致的finally块被执行的过程,非常类似于一个子函数(或子过程)被调用的过程。例如,当在 try 块中最后一条语句 return 被执行到的时候,一个展开操作便发生了,可以把展开操作想象成,是编译器在 return 语句之前插入了一些代码(这些代码完成对 finally 块的调用),因此可以得出结论:finally区域内的代码块,肯定是在 return 之前被执行。
不过请读者朋友注意:finally 块区域中的代码虽然在 return 语句之前被执行,但是 finally 块区域中的代码是不能够通过重新赋值的方式来改变 return 语句的返回值。请看如下的示例代码:
public class TestFinallyReturn{
public static void main(String[] args) {
// 你认为 test 函数返回的值是多少呢?
System.out.println("test 的返回值为: " + testFinally());
}
public static int testFinally() {
int tmp = 1;
try {
return tmp;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 注意,这里重新改变了 ret 的值。
tmp = 2;
}
return 0;
}
}
本来是想在finally区域中通过改变tmp的值,来影响testFinally函数最终return的值。但是真的影响了吗?答案是否定的。
虽然finally 块区域中的代码不会轻易影响try区域中return语句的返回值,但是有一种情况例外,那就是在finally内部使用 return 语句。如果把上例的finally区域的代码改为如下所示:
finally {
tmp = 2;
// 这里添加了一条 return 语句
return tmp;
}
由于finally区域中的代码先于return语句(try作用域中的)被执行,但是,如果此时在finally内部也有一个return语句,这将会导致该函数直接就返回了,而致使try作用域中的return语句再也得不到执行机会(实际就是无效代码,被覆盖了)。所以返回结果为2。
对上述情况,其实更合理的做法是,既不在try内部中使用return语句,也不在finally内部使用return语句,而应该在finally语句之后使用return来表示函数的结束和返回,把上面的程序改造一下,testFinally方法的代码如下:
public static int testFinally () {
int tmp = 1;
try {
//操作语句
} catch (Exception e) {
e.printStackTrace();
} finally {
tmp = 2;
}
// 把 return 语句放在最后,这最为妥当
return tmp;
}
9.5 getMessage和printStackTrace方法
在前面介绍过异常类有很多的子类,也建议过读者朋友不用去刻意记住这些子类。那么如何知道异常类的相关信息呢?这就需要借助本小节将要学习的两个方法:getMessage方法和printStackTrace方法。这两个方法的相关介绍如表9.2所示。
表9.2 getMessage方法和printStackTrace方法介绍
|
方 法 名 |
功 能 说 明 |
|
getMessage |
返回此Throwable对象的详细消息字符串 |
|
printStackTrace |
将此Throwable对象及其追踪输出至标准错误流。此方法将此Throwable对象的堆栈跟踪输出至错误输出流,作为字段System.err的值。输出的第一行包含此对象的toString()方法的结果。剩余行表示以前由方法fillInStackTrace()记录的数据 |
catch中声明的异常对象(catch(ExceptionName e))封装了异常事件发生的信息,在catch语句块中可以使用这个对象的一些方法获取这些信息。getMessage的用法示例如下。
try {
int i = 12 / 0;
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
运行这几行代码,输出:/ by zero。
printStackTrace的用法示例如下。
try {
int i = 12 / 0;
} catch(Exception ex) {
ex.printStackTrace();
}
运行上述代码,输出结果如图9.16所示。
使用printStackTrace()方法可以获取异常的具体类型,这样就可以在使用throw时抛出一个确切的异常。

图9.16 printStackTrace输出结果
9.6 多重catch处理异常
一段代码可能会引发多种类型的异常,当引发异常时,会按顺序来查看每个catch语句块,并执行第一个与异常类型匹配的catch语句块。执行其中的一个catch语句块之后,其后的catch语句块将被忽略。
多重catch语句块的常用方法如下所示。
public void method(){
try {
… // 代码段
… // 产生异常(异常类型2)
} catch (异常类型1 ex) {
… // 对异常进行处理的代码段
} catch (异常类型2 ex) {
… // 对异常进行处理的代码段
} catch (异常类型3 ex) {
… // 对异常进行处理的代码段
}
… // 代码段
}
多重catch语句块程序的运行流程如图9.17所示。

图9.17 多重catch语句块运行流程图
【示例7】MoreCatchDemo.java
通过MoreCatchDemo.java演示多重catch的用法。
import java.util.InputMismatchException;
import java.util.Scanner;
public class More CatchDemo {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
try {
System.out.print("请输入第一学期的总学时:");
int totalTime = in.nextInt(); // 总学时
System.out.print("请输入第一学期的课程数目:");
int totalCourse = in.nextInt(); // 课程数目
System.out.println("第一学期各课程的平均学时为:" + totalTime / totalCourse);
} catch (InputMismatchException e1) {
System.out.println("输入不为数字!");
} catch (ArithmeticException e2) {
System.out.println("课程数目不能为零!");
} catch (Exception e) {
System.out.println("发生错误:" + e.getMessage());
}
System.out.println("我是catch后面的代码");
}
}
输出结果为:
请输入第一学期的总学时:300h
输入不为数字!
我是catch后面的代码
根据输出结果可以看出,当匹配到异常类型InputMismatchException的对象e1后,e2、e都被忽略,直接执行catch语句块后面的代码。
在安排catch语句的顺序时,首先应该捕获子类异常,然后再捕获父类异常。如果顺序弄反了,后面捕获异常的代码将无法被调用。如果异常是同级关系,则无所谓哪个置前,哪个置后。如下列代码:
try {
FileInputStream fii = new FileInputStream("zah.txt");
fii.read();
} catch (Exception e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
在此处,第一个catch语句捕获了父类异常,所以后面的catch语句都不会被执行,编译器也不会接受这样的代码。
Java异常处理的几个原则如下:
(1)一个方法中可能会产生多种不同的异常,你可以设置多个“异常”抛出点来解决这个问题。
(2)“异常”对象从产生点产生后,到被捕捉后终止生命的全过程中,实际上是一个传值过程,所以你可以根据需要来合理的控制检测到“异常”的粒度。如果你并不需要知道具体产生的是什么异常,那么可以使用“异常”的公共父类Exception来结合“异常”对象,即catch(Exception e){…}。同样在“异常”与方法关联的传递过程中,也可以根据需要控制关联“异常”的粒度,即throws后面跟上异常对象的父类名。
(3) “异常机制”中还有一种特殊情况――RuntimeException“异常类”,这个“异常类”和它的所有子类都有一个特性,就是“异常”对象一产生就被Java虚拟机直接处理掉,即在方法中出现throw 子句的地方便被虚拟机捕捉了。因此凡是抛出这种“运行时异常”的方法在被引用时,不需要用try…catch语句来处理。
9.7 嵌套try-catch语句
你可以嵌套一个以上的 try...catch 语法。比如要完成从命令行传入参数求商的需求,就涉及到嵌套try-catch语句的应用,代码如下所示。
public class TryCatchDemo {
public static void main(String[] args) {
try {
int number1 = Integer.parseInt(args[0]);
int number2 = Integer.parseInt(args[1]);
try{
double result = number1 / number2;
}catch(Exception e){
System.out.println("/ by zero");
}
System.out.println("not exception is here!!");
} catch (Exception e) {
//System.out.println(e.getMessage());
e.printStackTrace();
}finally{
System.out.println("this is the end flag!");
}
}
}
try-catch嵌套运行时的先后顺序:先内后外。不过这种嵌套try-catch语句来处理异常的方式并不被笔者所荐。
9.8 自定义异常类
尽管Java中提供了众多的异常处理类,但程序设计人员有时候可能需要定义自己的异常类来处理某些问题,比如可以抛出中文文字的异常提示信息,帮助客户了解异常产生的原因。这种情况下用户只要定义一个直接或间接继承Throwable的类就可以了。一般情况下,自定义的异常类都选择Exception作为父类。
定义一个自定义的异常,命名为MyException.java。
【示例8】MyException.java
public class MyException extends Exception {
public MyException() {
super();
}
public MyException(String msg) {
super(msg);
}
public MyException(Throwable cause) {
super(cause);
}
public MyException(String msg, Throwable cause) {
super(msg, cause);
}
}
使用自定义异常类的代码如下所示。
public String[] createArray(int length) throws MyException {
if (length < 0) {
throw new MyException("数组长度小于0,不合法");
}
return new String[length];
}
对于初学读者,要注意,并不是对所有的方法都要进行异常处理,因为异常处理将占用一定的资源,影响程序的执行效率。在学习本章之后有以下建议:
l 认真观察抛出的异常的名字和行号。
l 调用Java方法前,阅读其API文档了解它可能会产生的异常。然后再据此决定是处理这些异常还是将其加入throws列表。
l 尽量减小try语句块的体积。
l 在处理异常时,应该打印出该异常的堆栈信息以方便调试。
一个try所包括的语句块,必须有对应的catch语句块或finally语句块。try语句块可以搭配多个catch语句块,catch语句块的捕获范围要由小到大。
9.9 本 章 练 习
1. 简答题
(1) Java语言如何进行异常处理?关键字throws、throw、try、catch、finally分别代表什么意义?在try块中可以抛出异常吗?
(2) 简单描述一下Java中的异常处理机制的原理和应用。
(3) try语句块里有一个return语句,那么紧跟在这个try语句块后的finally语句块里的代码会不会被执行?什么时候被执行?在return语句之前还是之后被执行?
(4) error和exception有什么区别?
(5) final、finally和finalize有什么用法上的区别?
2. 上机题
(1) 编写一个类ExceptionTest1,在main方法中使用try、catch、finally,要求:
① 在try块中,编写被零除的代码。
② 在catch块中,捕获被零除所产生的异常,并且打印异常信息。
③ 在finally块中,打印一条语句。
(2) 上机练习:写一个自定义异常,实现数组越界异常信息。
(3) 请在IDE中创建如下所示的TestEx类,运行之后观察输出结果是什么。
public class TestEx {
public static int test(int x) {
int i = 1;
try {
System.out.println("try块中10/x之前");
i = 10 / x;
System.out.println("try块中10/x之后");
return i;
} catch (Exception e) {
i = 100;
System.out.println("catch块中......");
} finally {
i = 1000;
System.out.println("finally块");
}
return i;
}
public static void main(String[] args) {
System.out.println(TestEx.test(1));
System.out.println(TestEx.test(0));
}
}
(4) 请在IDE中创建如下所示的TestEx类,运行之后观察输出结果是什么。
public class TestReturnException {
public static void main(String[] args) {
try {
System.out.println("testReturn 的返回值为: " + testReturn());
} catch (Exception e) {
e.printStackTrace();
}
}
public static int testReturn() throws RuntimeException {
int tmp = 0;
try {
System.out.println("try区域输出");
// 这里会导致出现一个运行态异常
int i = 4, j = 0;
tmp = i / j;
} catch (RuntimeException e) {
System.out.println("catch区域输出");
e.printStackTrace();
// 异常被重新抛出,上层函数可以进一步处理此异常
throw e;
} finally {
System.out.println("finally区域输出!");
// 注意,这里有一个 return 语句
return tmp;
}
}
}