对于初学JAVA的菜鸟,往往对异常处理不是很清楚。本文比较全面浅显的介绍下异常处理。
1.异常处理的优势
异常处理最根本的优势是将检测错误(由被调用的方法完成)从处理错误(由调用方法完成)中分离出来。
2.异常类型
Java API 中有很多预定义的异常类,下图给出其中的一部分。
Throwable类是所有异常类的根。所有的Java异常类都直接或间接地继承自Throwable。可以通过扩展Exception或者Exception的子类来创建自己的异常类。
这些异常类可以分为三种主要类型:系统错误,异常和运行时异常。
(1)系统异常(system error)是由Java虚拟机抛出的,用Error表示。Error类描述的是内部系统错误。这样的错误很少发生。如果发生,除了通知用户以及尽量稳妥地终止程序外,几乎什么也不做。
Error 类的子类的类子:
类 可能引起异常的原因
LinkageError 一个类对另一个类有某种依赖性,编译前者后,后者变得不兼容
VirtualMachineError Java虚拟机中断或者没有继续运行所必需的资源可用
(2)异常(exception)是用Eception类表示的,它描述的是由程序和外部环境引起的错误,这些错误能被程序捕获和处理。
Exception类的子类的例子:
类 可能引起异常的原因
ClassNotFoundException 企图使用一个不存在的类。例如,如果试图使用命令java来运
行
一个不存在的类,或者
程序要调用三个类文件而只能找到两
个,
都会发生这种异常
IOException 同输入/输出相关的操作,例如,无效的输入、读文件时超过
文件
尾、打开一个不存在的
文件等。IOException的子类的例
子有
InterruptedIOException、EOFException 和
FileNotFoundException
(3)运行时异常(runtime exception)是用RuntimeException类表示的,它描述的是程序设计错误,例如,错误的类型转换、访问一个越界的数组或数值错误。运行时异常通常是由Java虚拟机抛出的。
RuntimeException类的子类的例子:
类 可能引起异常的原因
ArithmeticException 一个整数除以0。注意,浮点数的算数运算不抛出异常。
NullPointerException 企图通过一个null引用变量访问一个对象
IndexOutOfBoundsException 数组的下标超出范围
IllegalArgumentException 传递给方法的参数非法或不合适
RuntimeException、Error以及它们的子类都称为免检异常(unchecked exception)。所有其他异常都称为必检异常(checked exception),意思是指编译器会强制程序员检查并处理它们。
在大多数情况下,免检异常都会反映出程序设计上不可恢复的逻辑错误。例如,如果通过一个引用变量访问一个对象之前并未将一个对象赋值给它,就会抛出NullPointerException异常;如果访问一个数组的越界元素,就会抛出IndexOutOfBoundsException异常。这些是程序中必须纠正的逻辑错误。免检异常可能在程序中的任何一个地方出现。未避免过多的使用try-catch块,Java语言允许不必编写代码捕获或声明免检异常。
3.Java异常处理三种操作:声明异常、捕获异常、抛出异常
(1).声明异常
为了在方法中声明一个异常,就要在方法头中使用关键字,如下:
public void myMethod() throws IOException
如果方法可能抛出多个异常,就可以在关键字throws后添加一个逗号分隔异常列表:
public void myMethod() throws Exception1, Exception2, ..., ExceptionN
注意
* 如果方法没有在父类中声明异常,那么就不能在子类中对其覆盖来声明异常。
(2).抛出异常
检测一个错误的程序可以创建一个正确类型的实例并抛出它。这就是抛出一个异常。比如程序发现传递给方法的参数与方法的合约不符,这个程序就可以创建IllegalArguemntException的一个实例并抛出它,如下:
IllegalArgumentException ex = new IlleagalArgumentException("Wrong Argument");
throw ex;
也可以使用下面的语句:
throw new IlleagalArgumentException("Wrong Argument");
(3).捕获异常
可以在try-catch块中捕获和处理异常。如下:
try
{
statements; // Statements that may throw exceptions
}
catch(Exception1 exVar1)
{
// handler for exception1;
}
catch(Exception2 exVar2)
{
// handler for exception2;
}
catch(Exception3 exVar3)
{
// handler for exception3;
}
如果在执行try块的过程中没有出现异常,则跳过catch子句
如果在try块中的某条语句抛出一个异常,Java就会跳过try快的剩余语句,然后开始检查处理这个异常的代码的过程。处理这个异常的代码成为
异常处理器(exception handler);可以从当前的方法开始,沿着方法调用链,按照异常的反向传播方向找到这个处理器。从第一个到最后一个逐个检查catch块,判断在catch块中的异常实例是否是该异常对象的类型。如果是,就将该异常对象赋值给所声明的变量,然后执行catch块中的代码。如果没有发现异常处理器,Java会退出这个方法,把异常传递给调用这个方法的方法,继续同样的过程来查找处理器。如果在调用的方法链中找不到处理器,程序就会终止并且在控制台上打印出错信息。寻找处理器的过程称为
捕获一个异常(catching an exception)。
如下:
main method
{
...
try
{
...
invoke method1;
statement1;
}
catch(Exception1 ex1);
{
Process ex1;
}
statement2;
}
method1
{
...
try
{
...
invoke method2;
statement3;
}
catch(Exception2 ex2)
{
Process ex2;
}
statement4;
}
method2
{
...
try
{
...
invoke method3;
statement3;
}
catch(Exception3 ex3)
{
Process ex3;
}
statement6;
}
method3 抛出一个异常
假设main方法调用method1, method1调用method2, method2调用method3, method3抛出一个异常,如上。考虑下面的情况:
1)如果异常类型是Exception3,它就会被method2中的处理异常ex3的catch块捕获。跳过statement5,然后执行statement6.
2)如果异常类型是Exception2,则退出method2,控制被返回给method1,而这个异常就会被method1中的处理异常ex2的catch块捕获。跳过
statement3,然后执行statement4.
3)如果异常类型是Exception1,则退出method1,控制被返回给main方法,而这个异常就会被main方法中处理异常ex1的catch块捕获。跳过
statement1,然后执行statement2.
4)如果异常没有在method2、method1和main方法中被捕获,程序就会终止。不执行statement1和statement2.
注意
* 从一个通用父类可以派生出各种异常类。如果一个catch块可以捕获一个父类的异常对象,它就能捕获那个父类的所有子类的异常对象。
注意 * 在catch块异常被指定的顺序是非常重要的。如果父类的catch块出现在子类的catch块之前,就会导致编译错误。例如,下面A的代码顺序是错误的,因为RuntimeException是Exception的一个子类。正确的顺寻是B。
// A
try
{
...
}
catch(Exception ex)
{
...
}
catch(RuntimeException ex)
{
...
}
// B
try
{
...
}
catch(RuntimeException ex)
{
...
}
catch(Exception ex)
{
...
}
注意 * Java强迫程序员处理必检异常。如果方法声明了一个必检异常,就必须在try-catch块中调用它,或者在调用方法中声明要抛出异常。例如,假定方法p1调用方法p2,而p2可能会抛出一个必检异常(例如, IOException),就必须按下图A和图B所示编写代码。
// A
void p1()
{
try
{
p2();
}
catch(IOException ex)
{
...
}
}
// B
void p1() throws IOException
{
p2();
}
4.从异常中获取信息
异常对象包含关于异常的有价值的信息。可以利用下面这些java.lang.Throwable类中的实例方法获取有关的异常的信息。
java.lang.Throwable
getMessage(): String 返回这个对象的消息
toString(): String 返回以下三个字符串的链接:(1)异常类的全名;(2)“:”
(冒号或空格)(3)getMessage()方法
printStacTrace(); void 在控制台上打印Throwable对象以及它的调用栈的跟踪信息
getStackTrace():StackTraceElement[] 返回栈跟踪元素构成的数组来表示这个可抛出的栈跟踪信息
下面给出一个例子:
public class TestException {
public static void main(String[] args) {
try {
System.out.println(sum(new int[] {1, 2, 3, 4, 5}));
}
catch (Exception ex) {
ex.printStackTrace();
System.out.println("\n" + ex.getMessage());
System.out.println("\n" + ex.toString());
System.out.println("\nTrace Info Obtained from getStackTrace");
StackTraceElement[] traceElements = ex.getStackTrace();
for (int i = 0; i < traceElements.length; i++) {
System.out.print("method " + traceElements[i].getMethodName());
System.out.print("(" + traceElements[i].getClassName() + ":");
System.out.println(traceElements[i].getLineNumber() + ")");
}
}
}
private static int sum(int[] list) {
int result = 0;
for (int i = 0; i <= list.length; i++)
result += list[i];
return result;
}
}
输出的信息如下图:
5.finally子句
有时候,不论异常是否出现或者被捕获,都希望执行某些代码块。finally子句可以实现这个目的。语法如下:
try
{
statement;
}
catch(TheException ex)
{
// handling ex;
}
finally
{
// finalStatements;
}
6.何时使用异常
try块包含正常情况下执行的代码。catch块包含异常情况下执行的代码。异常处理将错误处理代码从正常的程序设计任务中分离出来,这样,可以使程序更易读和更易修改。但是,应该注意,由于异常处理需要初始化新的异常对象,需要从调用栈返回,而且还需要沿着方法调用链来传播异常以便找到它的异常处理器,所以,异常处理通常需要更多的时间和资源。
异常出现在方法中。如果想让该方法的调用者处理异常,应该创建一个异常对象并将其抛出。如果能在发生异常的方法中处理异常,那么就不需要抛出或使用异常。
一般来说,一个项目中多个类都会发生的共同异常应该考虑作为一种异常类。对于发生在个别方法中的简单错误最好进行局部处理,无须抛出异常。
在代码中,什么时候应该使用try-catch块呢?当必须处理不可预料的错误状况时应该使用它。不要用try-catch块处理简单的、可预料的情况。如下:
try
{
System.out.println(refVar.toString());
}
catch(NullPointerException ex)
{
System.out.println("refVar is null");
}
最好用以下代码代替:
if(refVar != null)
System.out.println(refVar.toString());
else
System.out.println("refVar is null");
哪些情况是异常的,哪些情况是可预料的,有时候很难判断。但有一点要把握住,不要把异常处理用作简单的逻辑测试。
7.重新抛出异常
如果异常处理器没有处理异常,或者处理器只是希望它的调用者注意到该异常,Java就允许异常处理器重新抛出该异常。语法如下:
try
{
statement;
}
catch(TheException ex)
{
// perform operations berfore exists;
throws ex;
}
语句throws ex重新抛出异常给调用者,以便调用者的其他处理器获得处理异常ex的机会。
8.链式异常
有时候,可能需要同原始异常一起抛出一个新异常(带附加信息)。这称为链式异常(chained exception)。下面给出一个例子:
public class ChainedExceptionDemo {
public static void main(String[] args) {
try {
method1();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
public static void method1() throws Exception {
try {
method2();
}
catch (Exception ex) {
throw new Exception("New info from method1", ex);
}
}
public static void method2() throws Exception {
throw new Exception("New info from method2");
}
}
执行结果如下图:
main方法调用method1(第四行),method1调用么method2(第十三行),method2抛出一个异常(第二十一行)。该异常被method1的catch块所捕获,并在第16行包装成一个新异常。该异常被抛出,并在main方法中的第4行的catch中被捕获。示例输出在第7行中的printStackTrace()方法的结果。首先,显示method1中抛出的异常,然后显示method2中抛出的原始异常。
9.创建自定制异常类
Java提供相当多的异常类,尽量使用它们而不要创建自己的异常类。然而,如果遇到一个不能用预定义异常类描述恰当的问题,那就可以通过派生Exception类或其子类来创建自己的异常类。示例如下:
public class InvalidRadiusException extends Exception {
private double radius;
/** Construct an exception */
public InvalidRadiusException(double radius) {
super("Invalid radius " + radius);
this.radius = radius;
}
/** Return the radius */
public double getRadius() {
return radius;
}
}
第六行调用父类的带有一条消息的构造方法。这条消息将会被设置在异常对象中,并且可以通过在对象上调用getMessage()获得。
注意 * 不要扩展RuntimeException声明一个自定制异常类。因为这会使自定制异常成为免检的。最好使用自定制必检异常类。