异常处理
任何一个软件或程序都可能在运行的过程中出现故障,问题的关键是故障出现以后如何处理?谁来处理?怎样处理?处理后系统能否恢复正常的运行?本章在介绍Java处理这类问题基本方法的基础上,讨论包含异常处理的Java程序的设计方法。
1、异常和异常处理的两种方法
异常是指程序在执行过程中出现的意外事件。异常通常会使程序的正常流程被打断。例如,程序在向磁盘中读写文件时磁盘出现问题、算术运算中被除数为0、数组下标越界、输入/输出时文件不存在、输入数据格式不正确、程序本身错误,等等。
一般情况下,程序中需要处理异常情况,通过对异常情况的处理,可以使程序的执行流程继续下去,并进行一些异常处理;否则,程序的正常执行流程会被中断。
1、异常的基本类型
异常处理的第一步是确定异常的类型,常见的异常情况一般分为以下几类:
用户输入错误:指用户键盘输入错误,输入格式不对或输入内容不符合程序要求等。例如,用户界面要求输入一个整数,而用户输入了一个字符串,或者要求用户输入一个URL地址,但用户输入的地址语法错误等。
设备故障:计算机硬件设备有时也会出故障,例如打印机没连接好,要求的网页没有找到等。
物理限制:物理设备本身的限制,例如硬盘已存满,内存已用完等。
代码错误:程序员编写的代码出现错误,例如从一个空的堆栈中弹出元素,数组下标为负数等。
2、if-else形式的异常处理方法
用if-else语句(或switch-case语句)可以发现异常并做出相应处理。例如,在程序中要将一个文件整个读入内存,通常的步骤是:
(1)打开文件。
(2)判断文件的大小。
(3)分配内存。
(4)将文件读入内存。
(5)关闭文件。
在上述的过程中可能出现很多问题,例如:
(1)文件打不开。
(2)不能决定文件的大小
(3)内存不能分配
(4)读入失败
(5)文件不能关闭
针对这些情况,需要在程序中做出相应的处理,若用if-else语句,则程序伪码如下:
Class ReadFile
{
初始化 错误代码 = 0;
打开文件;
if (文件可以打开)
{
计算文件的长度;
if (文件的长度可以决定)
{
分配内存
if (有足够的内存)
{
将文件读入内存
if (读入失败)
{
错误代码 = "读入文件时失败"
}
}
else
{
错误代码 = "内存不足"
}
}
else
{
错误代码 = "不能决定文件的大小"
}
关闭文件;
if (文件没有关闭 && 错误代码 == 0)
{
错误代码 = "文件关闭失败"
}
}
else
{
错误代码 = "文件打不开"
}
return错误代码
}
用if-else语句方式可以发现异常并做出相应的处理。但是,程序员由于过多地分析程序中异常情况的发生情况,在程序中过多地使用if-else语句,会使程序员正常的程序设计思路受到影响。另外,程序中过多地使用if-else语句,也会影响程序的算法思想的可读性。而且,会更为重要的是,有些异常情况是不可预见的。例如计算机的连接中断,打印机纸张用完等。所以,if-else形式不是处理异常的好的方法.
3、
Java的异常处理方法
在Java语言中,用try模块和catch模块把程序的正常流程代码和异常处理代码分离。
对于上述例子,按照Java的异常处理方式,伪码表示的程序代码如下:
Class ReadFile
{
try
{
打开文件
计算文件的长度
分配内存
将文件读入内存
关闭文件
}
catch (文件打开失败异常)
{
处理文件打开失败异常
}
catch (文件的长度不能确定异常)
{
处理文件的长度不能确定异常
}
catch (分配内存失败异常)
{
处理分配内存失败异常
}
catch (文件读入内存失败异常)
{
处理文件读入内存失败异常
}
catch (关闭文件失败异常)
{
处理关闭文件失败异常
}
}
从上述伪码可以看出,程序中可能出现的异常情况都放进了try模块中,而对于各种异常情况,设计了相应的catch模块,这些模块可以用来捕捉这些异常情况,并进行相应的处理。
在Java程序中,如果设计人员对可能出现的异常没有设计相应的try模块和catch模块,或设计人员无法预见的异常情况,系统会将出现的异常交由Java虚拟机(JVM)处理,此时Java虚拟机会自动捕捉这些异常情况,并将异常情况在屏幕上显示出来。
2、Java的异常类
Java语言对大多数常见的异常都定义了异常类。这些异常类可以分为两大类:Error类和Exception类。
Error(错误)类和Exception (异常)类的区别在于:错误指的是系统异常或运行环境出现的异常,这些异常一般是很严重的异常,即使捕捉到通常也无法处理,例如Java虚拟机异常。而Exception类的异常指的是一般的异常,例如,输入/输出(I/O)异常、数据库访问(SQL)异常等。对这些异常应用程序可以进行处理。Java的所有异常类都继承自Throwable类。异常类是Java语言包(java.lang)中的类。
Java异常类的继承关系如图所示:
1、Error类及其子类
当系统动态连接失败,或者出现虚拟机的其他故障时,Java虚拟机会抛出Error错误。程序一般不用捕捉Error错误,由系统进行这类错误的捕捉和处理。Error类及其子类定义了系统中大多数这类错误。
这里介绍Error类的两个子类及其子类:LinkageError(结合错误)类及其子类和VirtualMachineErrror(虚拟机错误)类及其子类。
(1)LinkageError类及其子类
LinkageError(结合错误)类及其子类定义的是一个类依存于另一个类,但在编译前者时后者出现了与前者不兼容情况的各种错误。
LinkageError类的子类有:
ClassFormatError 类格式错误
ClassCircularityError 类循环错误
NoClassDefFoundError 类定义无法找到错误
VerifyError 校验错误
AbstractMethodError 抽象方法错误
NoSuchFieldError 没有成员变量错误
InstantiationError 实例错误
(2)VirtualMachineErrror类及其子类
VirtualMachineErrror(虚拟机错误)类及其子类定义的是系统在虚拟机损坏或需要运行程序的资源耗尽时出现的各种错误。
VirtualMachineErrror类的子类有:
InternalError 内部错误
OutOfMemoryError 内存溢出错误
StackOverflowError 堆栈溢出错误
UnkownError 未知错误
2、Exception类及其子类
Exception类及其子类定义了程序中大多数可以处理的异常。
这里介绍Exception类的两个子类及其子类:RuntimeException (运行时异常)类及其子类和CheckedException (检查异常) 类及其子类。
(1)RuntimeException类及其子类
RuntimeException(运行时异常)类及其子类定义的是Java程序执行过程中可能出现的各种异常,RuntimeException类的子类有:
ArithmeticException 算术运算异常
ArrayStoreException 数组存储异常
ArrayIndexOutOfBoundsException 数组下标越界异常
CaseCastException 类型转换异常
IllegalArgumentException 非法参数异常
IllegalThreadStateException 非法线程状态异常
NumberFormatException 数字格式异常
IlegalMonitorStateException 非法监视状态异常
IndexOutofBoundsException 下标超出范围异常
NegativeArraySizeEXception 负数组个数异常
NullPointerException 空指针异常
SecurityException 安全异常
EmptyStackException 空栈异常
NoSuchElementException 没有元素异常
(2)CheckedException 及其子类
CheckedException (检查异常)类及其子类定义的是Java程序编译时编译器发现的各种异常。
CheckedException类的子类有:
ClassNotFoundException 类找不到异常
CloneNotSupportedException 复制不支持异常
IllegalAccessException 非法访问异常
InstantiationException 实例异常
InterruptedException 中断异常
IOException 输入/输出异常
FileNotFoundException 文件找不到异常
InterruptedIOException 中断输入/输出异常
3、Throwable类的方法
Error类和Exception类的方法基本都是从Throwable类中继承来的。
Throwable类的构造方法主要有:
Throwable() //构造一个对象,其错误信息串为null
Throwable(String message) //构造一个对象,其错误信息串为message
Throwable类的方法主要有:
String getMessage() 返回对象的错误信息
void printStackTrace() 输出对象的跟踪信息到标准错误输出流
void printStackTrace(PrintStream s) 输出对象的跟踪信息到输出流s
String toString() 返回对象的简短描述信息
4、异常类的对象
应用程序中一旦出现异常,系统会产生一个异常类的对象,下面讨论的catch语句后面的参数将接收到这样的一个对象。
为了语言简洁,通常把异常类的对象也简称异常。这就像第6章中我们也把组件类的对象简称组件。
3、Java的异常处理方法
在Java程序中,用try-catch(或try-catch-finally)语句来抛出、捕捉和处理异常。
try-catch-finally语句的语法格式是:
try
{
可能抛出异常的语句模块;
}
catch (异常类型1)
{
处理异常类型1语句;
}
……
catch (异常类型n)
{
处理异常类型n语句;
}
finally
{
无论是否抛出异常都要执行的语句;
}
try-catch-finally语句的功能为:
(1)try模块:
所有可能抛出异常的语句都放入try模块中。try模块中的语句是程序正常流程要执行的语句,但是在执行过程中有可能出现异常。如果像前面程序设计例子那样没有使用try模块,则除了系统定义的异常外,语句执行过程中出现的其他异常不会被抛出。
(2)catch模块:
主要负责对抛出的异常做相应的处理。所有抛出的异常都必须是Throwable类或其子类的对象。try模块中的语句可能抛出很多不同类型的异常,所以需要针对不同类型的异常分别设计catch模块。一般程序中会有若干个catch模块,每一模块处理一种类型的异常。
对于这些catch模块,其排列的先后顺序有如下规定:出现在前面的catch模块中的异常类一定要是后面的catch模块中的异常类的子类, 即前面的catch模块中的异常类对象一定比后面的catch模块中的异常类对象特殊。这是因为catch模块是按顺序执行的。如果try模块抛出异常,系统会按catch模块的排列次序依次查找,如果前面catch模块的异常类型匹配不上,就按顺序和后面的catch模块匹配。如果在所有的catch模块中都匹配不上,就交给系统的虚拟机,由虚拟机来处理这个异常。
程序中即使没有try模块,若出现了系统定义的异常,系统也会自动抛出,并由系统负责捕捉和处理;但是程序中若有了catch模块,则一定要有try模块。另外,若用户希望按照自己的意图处理程序中可能出现的系统定义的异常,也要在程序中使用try-catch语句,否则出现的异常将由系统捕捉和处理。
(3)finally模块:
finally模块中的语句是必须执行的语句。无论try模块中是否抛出异常,finally模块中的语句时都要被执行。这个模块是可选的。如果设计了finally模块,通常在这个模块中做一些资源回收的工作。
【例8.1】try-catch-finally的执行过程示例。
要求:设计一个用命令行参数输入数据的程序,若该参数的数值输入的不正确会出现异常。
程序设计如下:
public class TryCatchSequence
{
public static void main(String args[])
{
int a,b,c;
try
{
a=100;
b=Integer.parseInt(args[0]); //把命令行输入的字符串转换成整数
c=a/b;
System.out.println("c="+c);
System.out.println("No exception.");
}
catch(ArrayIndexOutOfBoundsException e) //数组下标越界异常
{
System.out.println("Exception caught!");
e.printStackTrace();
}
catch(ArithmeticException e) //处理算术运算异常
{
System.out.println("Exception caught!");
e.printStackTrace();
}
finally
{
System.out.println("Always executed");
}
}
}
我们分以下几种情况分别运行上述程序:
(1) 没有异常抛出。在当前工作目录下键入如下命令(注意:该程序正常运行需要从命令行给出参数,且该参数值不能为0):
java TryCatchSequence 10
则程序的运行结果如下:
c=10
No exception.
Always executed
程序的运行结果说明:系统执行完了try模块的所有程序, 由于程序运行时没有异常抛出,所以系统跳过catch模块, 最后执行了finally模块。
(2) 有异常抛出。上述程序可能有如下两种类型的异常发生。
--抛出数组下标越界异常。
在当前工作目录下键入如下命令(注意:该程序运行时无法从命令行得到参数值):
java TryCatchSequence
程序运行结果如下:
Exception caught!
java.lang.ArrayIndexOutOfBoundsException: 0
at TryCatchSequence.main(TryCatchSequence.java:9)
Always executed
程序的运行结果说明:由于命令行没有给出参数,在执行语句:
b=Integer.parseInt(args[0]);
时,系统抛出了数组下标越界异常。程序中处理数组下标越界异常的catch模块匹配上了该异常。程序执行完该catch模块语句后,跳过后面的其它catch模块,又执行了finally模块中的语句。
--抛出算术运算异常。
在当前工作目录下键入如下命令(注意:该程序运行时从命令行得到的参数值为0):
java TryCatchSequence 0
程序运行结果如下:
Exception caught!
java.lang.ArithmeticException: / by zero
at TryCatchSequence.main(TryCatchSequence.java:10)
Always executed
程序的运行结果说明:由于命令行给出的参数为0,程序中的被除数为0,因此运行时系统抛出了算术运算异常。程序中处理算术运算异常的catch模块匹配上了该异常。程序执行完该catch模块语句后,跳过后面的其它catch模块,又执行了finally模块中的语句。
【例8.2】程序中设计的catch模块捕捉不到异常的示例。
要求:程序中设计了若干catch模块,但忽略了其中某种可能发生的异常。
程序设计如下:
public class TryCatchSequence1
{
public static void main(String args[])
{
int a,b,c;
try
{
a=100;
b=Integer.parseInt(args[0]);
c=a/b;
System.out.println("c="+c);
System.out.println("No exception.");
}
catch(ArithmeticException e)
{
System.out.println("Exception caught!");
e.printStackTrace();
}
finally
{
System.out.println("Always executed");
}
}
}
程序设计说明:上述程序和例8.1的程序基本相同,只是去掉了前面程序的catch(ArrayIndexOutOfBoundsException e)模块。
程序运行结果如下:
Always executed
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at TryCatchSequence2.main(TryCatchSequence2.java:9)
程序的运行结果说明:程序中出现的ArrayIndexOutOfBoundsException异常不能和程序中的任何catch模块匹配,因此交给系统处理,系统内置的异常处理模块输出了相应的错误信息。
4、异常的抛出和处理
系统的异常类定义了很多异常,如果程序运行时出现了系统定义的异常,系统会自动抛出。此时,若应用程序中有try-catch语句,则这些异常由系统捕捉并交给应用程序处理;若应用程序中没有try-catch语句,则这些异常由系统捕捉和处理。
对于系统定义的有些应用程序可以处理的异常,一般情况下并不希望由系统来捕捉和处理,也不希望这种异常造成后果,因为这两种情况都有可能造成运行的应用程序产生不良后果。这种情况下,设计应用程序的一般方法是:在try模块中,应用程序自己判断是否有异常出现,如果有异常出现,则创建异常对象并用throw语句抛出该异常对象;在catch模块中,设计用户自己希望的异常处理方法。
throw语句的语法格式为:
throw <异常类对象>;
这种情况下,异常的抛出和处理有两种方式:一种是在同一个方法中抛出异常和处理异常,另一种是在一个方法中抛出异常,在调用该方法的方法中处理异常。
1、在同一个方法中抛出异常和处理异常
在同一个方法中抛出异常和处理异常。应用程序中,大部分异常的抛出和异常的处理采用这种设计方法。
【例8.3】在同一个方法中抛出异常和处理异常示例。
要求:设计一个堆栈类,堆栈类中要求有一个入栈方法,该方法向堆栈中加入一个元素。当调用入栈方法且堆栈已满时会出现堆栈已满异常。处理异常的模块捕捉到该异常后,显示异常信息后退出系统。
程序设计如下:
class SeqStack //堆栈类
{
int data[]; //成员变量,存放元素
int MAX; //成员变量,数组个数
int top; //成员变量,当前元素个数
SeqStack(int m) //构造方法
{
MAX = m; //为MAX赋值m
data = new int[MAX]; //创建数组对象data
top = 0; //初始值为0
}
public void push(int item) //入栈方法
{
try //try模块
{
if(top == MAX) //判断是否有异常产生
throw new Exception("stackFull"); //抛出异常对象
data[top] = item; //在堆栈中加入元素item
top++; //元素个数加1
}
catch(Exception e) //catch模块
{
System.out.println("exception:"+e.getMessage());
System.exit(0);
}
}
public static void main(String args[])
{
SeqStack stack = new SeqStack(10); //创建对象stack
for(int i = 0; i <11; i++)
stack.push(i); //调用push()方法
}
}
程序运行结果如下:
exception:stackFull
程序设计说明:
(1)main()中第一条语句创建的对象stack的数组个数MAX为10,但循环语句最后一次执行时,要向对象stack的数组data中加入第11个元素,所以此时会产生异常。
(2)由于push()方法中的异常情况(top == MAX)不是系统定义的异常,所以,系统不会抛出该异常,需要在该方法中用throw语句自己抛出异常。
(3)抛出异常语句throw new Exception("stackFull")首先调用Exception类的构造方法创建一个异常信息为"stackFull"的异常对象,然后抛出该异常。
(4)push()方法中的catch模块捕捉到异常后进行了所要求的异常处理。
2、抛出异常和处理异常的方法不是同一个
如果抛出异常的方法和处理异常的方法不是同一个方法时,则要求在抛出异常的方法定义后加如下语句:
throws Exception
然后,把catch模块放在调用该方法的方法中。
throws Exception语句的功能,是在调用方法和可能产生异常的被调用方法之间建立起系统处理异常所需的联系。
【例8.4】抛出异常和处理异常的方法不是同一个示例。
要求:问题同例8.3,但要求抛出异常和处理异常的方法不是同一个。
程序设计如下:
class SeqStack2
{
int data[];
int MAX;
int top;
SeqStack2(int m)
{
MAX = m;
data = new int[MAX];
top = 0;
}
public void push(int item) throws Exception //加throws语句
{
if(top == MAX) //判断是否有异常产生
throw new Exception("stackFull"); //抛出异常
data[top] = item;
top++;
}
public static void main(String args[])
{
SeqStack2 stack = new SeqStack2(10);
try //try模块
{
for(int i = 0; i <11; i++)
stack.push(i);
}
catch(Exception e) //catch模块
{
System.out.println("exception:"+e.getMessage());
System.exit(0);
}
}
}
程序运行结果和例8.3相同。
需要说明的是,此例子只是为了说明此种设计方法,像例8.3这类问题应用程序的异常抛出和异常处理通常采用前一种设计方法。
但是,Java API中的许多可能出现异常的方法都采用此种设计方法。例如,第9章讨论的输入输出流类中的许多方法在调用时都可能出现异常,而调用这些方法的方法是在用户编写的应用程序中,Java API不可能设计出适合所有应用情况的异常处理方法,因此,Java API的这些方法中没有设计try模块和catch模块,只是在方法定义后添加了throws语句。如下方法定义就是一个例子:
public void close() throws IOException
//表示close()方法可能抛出IOException异常
如果应用程序调用该方法,一般情况下,要在调用方法中设计try模块和catch模块,来处理IOException异常。IOException类是Exception类的一个子类。
5、自定义的异常类
应用程序中除了可能出现系统定义的异常外,有时还可能出现系统没有考虑的异常。此时需要在应用程序中自定义异常类。一般情况下,自定义的异常类都是一些应用程序可以处理的异常。所以,自定义的异常类一般是Exception类的子类。
【例8.5】自定义的异常类示例。
class UserDefinedException extends Exception
{
String msg;
public UserDefinedException()
{
this.msg = "";
}
public UserDefinedException(String s)
{
this.msg = s;
}
}
例8.5中,用户自己定义的异常类UserDefinedException继承了类Exception,类UserDefinedException中定义了两种情况的异常,一种的参数为空,另一种的参数为字符串s。
-The End-