1 Java的异常控制机制
捕获错误最理想的是在编译期,最好在试图运行程序以前。然而,并非所有错误都能在编译期间侦测到。有些问题必须在运行期间解决。让错误的缔结者通过一定的方法预先向接收者传递一些适当的信息,使其知道可能发生什么样的错误以及该如何处理遇到的问题,这就是Java的异常控制机制。
“异常”(Exception)这个词表达的是一种正常情况之外的“异常”。在问题发生的时候,我们可能不知具体该如何解决,但肯定知道已不能不顾一切地继续下去。此时,必须坚决地停下来,并由某人、某地指出发生了什么事情,以及该采取何种对策。异常机制的另一项好处就是能够简化错误控制代码。我们再也不用检查一个特定的错误,然后在程序的多处地方对其进行控制。此外,也不需要在方法调用的时候检查错误(因为保证有人能捕获这里的错误)。我们只需要在一个地方处理问题:“异常控制模块”或者“异常控制器”。这样可有效减少代码量,并将那些用于描述具体操作的代码与专门纠正错误的代码分隔开。一般情况下,用于读取、写入以及调试的代码会变得更富有条理。
若某个方法产生一个异常,必须保证该异常能被捕获,并获得正确对待。Java的异常控制机制的一个好处就是允许我们在一个地方将精力集中在要解决的问题上,然后在另一个地方对待来自那个代码内部的错误。那个可能发生异常的地方叫做“警戒区”,它是一个语句块,我们有必要派遣警探日夜监视着。生成的异常必须在某个地方被捕获和进行处理,就象警察抓到嫌疑犯后要带到警署去询问。这个地方便是异常控制模块。
“警戒区”是一个try关键字开头后面用花括号括起来的语句块,我们把它叫作“try块”。当try块中有语句发生异常时就掷出某种异常类的一个对象。异常被异常控制器捕获和处理,异常控制器紧接在try块后面,且用catch关键字标记,因此叫做“catch块”。catch块可以有多个,每一个用来处理一个相应的异常,因为在“警戒区”内可能发生的异常种类不止一个。所以,异常处理语句的一般格式是:
try {
// 可能产生异常的代码
}
catch (异常对象 e) {
//异常 e的处理语句
}catch (异常对象 e1) {
//异常 e的处理语句
}catch (异常对象 e2) {
//异常 e的处理语句
}
即使不使用try-catch结构,发生异常时Java的异常控制机制也会捕获该异常,输出异常的名称并从异常发生的位置打印一个堆栈跟踪。然后立即终止程序的运行。下面的例子发生了一个“零除”异常,后面的hello没有被打印。
例1 没有作异常控制的程序。
///
public
class
Exception1 {
public
static
void
main(String args[]) {
int
b = 0;
int
a = 3 / b;
System.out.println(
"Hello!"
);
}
}
///
输出结果:
java.lang.ArithmeticException: / by zero
at Exception1.main(Exception1.java:5)
Exception in thread "main" Exit code: 1
There were errors
但是如果使用了try-catch来处理异常,那么在打印出异常信息后,程序还将继续运行下去。下面是处理了的代码。
///
// Exception2.java
public
class
Exception2 {
public
static
void
main(String args[]) {
try {
int
b = 0;
int
a = 3 / b;
}
catch(ArithmeticException e) {
e.printStackTrace
}
System.out.println(
"Hello!"
);
}
}
///
输出结果:
Exception:
java.lang.ArithmeticException: / by zero
at Exception2.main(Exception1.java:5)
Hello!
与前例不同的是,Hello!被输出了。这就是try-catch结构的用处,它使异常发生和处理后程序得以“恢复”而不是“中断”。
2 异常类、异常规范和throw语句
为了使异常控制机制更出色地发挥它的功效,Java设计者几乎所以可能发生的异常,预制了各色各样的异常类和错误类。它们都是从“可掷出”类Throwable继承而来的,它派生出两个类Error和Exception。由Error派生的子类命名为XXXError,其中词XXX是描述错误类型的词。由Exception派生的子类命名为XXXException,其中词XXX是描述异常类型的词。Error类处理的是运行使系统发生的内部错误,是不可恢复的,唯一的办法只要终止运行运行程序。因此,客户程序员只要掌握和处理好Exception类就可以了。
Exception类是一切异常的根。现成的异常类非常之多,我们不可能也没有必要全部掌握它。好在异常类的命名规则大致描述出了该类的用途,而异常类的方法基本是一样的。下面给出lang包中声明的部分异常类。
RuntimeException 运行时异常
NullPointerException 数据没有初始化就使用
IndexOutOfBoundsException 数组或字符串索引越界
NoSuchFieldException 文件找不到
NoSuchMethodException 方法没有定义
ArithmeticException 非法算术运行
在其他包中也有相关的异常类,例如io包中有IOEception类。利用异常的命名规则,你可以使用下面的DOS命令在包所在的目录查看有什么异常类可用:
DIR *Eception.class
对于运行时异常RuntimeException,我们没必要专门为它写一个异常控制器,因为它们是由于编程不严谨而造成的逻辑错误。只要让出现终止,它会自动得到处理。需要程序员进行异常处理的是那些非运行期异常。
Throwable有三个基本方法:
-
String getMessage() 获得详细的消息。
-
String toString() 返回对本类的一段简要说明,其中包括详细的消息(如果有的话)。
-
void printStackTrace() 或 void printStackTrace(PrintStream)
打印出调用堆栈路径。调用堆栈显示出将我们带到异常发生地点的方法调用的顺序。
因为Exception类是一切异常的根,所以对任何一个现有的异常类都可以使用上述方法。
异常规范
和
throws
java库程序员为了使客户程序员准确地知道要编写什么代码来捕获所有潜在的异常,采用一种叫做throws的语法结构。它用来通知那些要调用方法的客户程序员,他们可能从自己的方法里“掷”出什么样的异常。这便是所谓的“异常规范”,它属于方法声明的一部分,即在自变量(参数)列表的后面加上throws 异常类列表。例如
void f() throws tooBig, tooSmall, divZero { 方法体}
若使用下述代码:
void f() [ // ...
它意味着不会从方法里“掷”出异常(除类型为RuntimeException的异常以外,它可能从任何地方掷出)。
如果一个方法使用了异常规范,我们在调用它时必须使用try-catch结构来捕获和处理异常规范所指示的异常,否则编译程序会报错而不能通过编译。这正是Java的异常控制机制的杰出贡献,它对可能发生的意外及早预防从而加强了代码的健壮性。
在使用了异常规范的方法声明中,库程序员使用throw语句来掷出一个异常。throw语句的格式为:
thrownew XXXException();
由此可见,throw语句掷出的是XXX类型的异常的对象(隐式的句柄)。而catch控制器捕获对象时要给出一个句柄 catch(XXXException e)。
我们也可以采取“欺骗手段”,用throw语句“掷”出一个并没有发生的异常。编译器能理解我们的要求,并强迫使用这个方法的用户当作真的产生了那个异常处理。在实际应用中,可将其作为那个异常的一个“占位符”使用。这样一来,以后可以方便地产生实际的异常,毋需修改现有的代码。下面我们用“欺骗手段”给出一个捕获异常的示例程序。
例2 本例程演示异常类的常用方法。
///
public
class ExceptionMethods {
publicstaticvoid main(String[] args) {
try {
thrownew Exception("Here's my Exception");
} catch(Exception e) {
System.out.println("Caught Exception");
System.out.println(
"e.getMessage(): " + e.getMessage());
System.out.println(
"e.toString(): " + e.toString());
System.out.println("e.printStackTrace():");
e.printStackTrace();
}
}
}
///
该程序输出如下:
Caught Exception
e.getMessage(): Here's my Exception
e.toString(): java.lang.Exception: Here's my Exception
e.printStackTrace():
java.lang.Exception: Here's my Exception
at ExceptionMethods.main
在一个try区中潜在的异常可能是多种类型的,那时我们需要用多个catch块来捕获和处理这些异常。但异常发生时掷出了某类异常对象,Java依次逐个检查这些异常控制器,发现与掷出的异常类型匹配时就执行那以段处理代码,而其余的不会被执行。为了防止可能遗漏了某一类异常控制器,可以放置一个捕获Exception类的控制器。Exception是可以从任何类方法中“掷”出的基本类型。但是它必须放在最后一个位置,因为它能够截获任何异常,从而使后面具体的异常控制器不起作用。下面的示例说明了这一点。
例3 本例程演示多个异常控制器的排列次序的作用。
///
public
class
MutilCatch {
private staticvoid test(int
i) {
try
{
int
x = i;
if
(x>0)
throw
new
ArithmeticException (
"this is a Arithmetic Exception!"
);
else
if
(x<0)
throw
new
NullPointerException (
"this is a NullPointer Exception!"
);
else
throw
new
Exception(
"this is a Exception!"
);
}
catch
(ArithmeticException e) {
System.out.println(e.toString());
}
catch
(NullPointerException e) {
System.out.println(e.toString());
}
catch
(Exception e) {
System.out.println(e.toString());
}
}
public
static
void
main(String[] args) {
test(-1); test(0); test(1);
}
}
///
运行结果:
java.lang.NullPointerException: this is a NullPointer Exception!
java.lang.Exception: this is a Exception!
java.lang.ArithmeticException: this is a Arithmetic Exception!
如果你把捕获Exception的catch放在前面,编译就通不过。
3 用finally清理
我们经常会遇到这样的情况,无论一个异常是否发生,必须执行某些特定的代码。比如文件已经打开,关闭文件是必须的。但是,在try区内位于异常发生点以后的代码,在发生异常后不会被执行。在catch区中的代码在异常没有发生的情况下不会被执行。为了无论异常是否发生都要执行的代码,可在所有异常控制器的末尾使用一个finally从句,在finally块中放置这些代码。(但在恢复内存时一般都不需要,因为垃圾收集器会自动照料一切。)所以完整的异常控制结构象下面这个样子:
try { 警戒区域 }
catch (A a1) { 控制器 A }
catch (B b1) { 控制器 B }
catch (C c1) { 控制器 C }
finally { 必须执行的代码}
例4 演示finally从句的程序。
///
// FinallyWorks.java
// The finally clause is always executed
public
class FinallyWorks {
staticint count = 0;
publicstaticvoid main(String[] args) {
while(true) {
try {
// post-increment is zero first time:
if(count++ == 0)
thrownew Exception();
System.out.println("No exception");
} catch(Exception e) {
System.out.println("Exception thrown");
} finally {
System.out.println("in finally clause");
if(count == 2) break; // out of "while"
}
}
}
}
///
运行结果:
Exception thrown
in finally clause
No exception
in finally clause
一开始count=0发生异常,然后进入finally块;进入循环第二轮没有异常,但又执行一次finally块,并在其中跳出循环。
下面我们给出一个有的实用但较为复杂一点的程序。我们创建了一个InputFile的类。它的作用是打开一个文件,然后每次读取它的一行内容。
例5 读文本文件并显示到屏幕上。
///
//: Cleanup.java
// Paying attention to exceptions in constructors
import java.io.*;
class InputFile {
private BufferedReader in;
InputFile(String fname) throws Exception {
try {
in = new BufferedReader(new FileReader(fname));
// Other code that might throw exceptions
} catch(FileNotFoundException e) {
System.out.println("Could not open " + fname);
// Wasn't open, so don't close it
throw e;
} catch(Exception e) {
// All other exceptions must close it
try {
in.close();
} catch(IOException e2) {
System.out.println("in.close() unsuccessful");
}
throw e;
} finally {
// Don't close it here!!!
}
}
String getLine() {
String s;
try {
s = in.readLine();
} catch(IOException e) {
System.out.println("readLine() unsuccessful");
s = "failed";
}
return s;
}
void cleanup() {
try {
in.close();
} catch(IOException e2) {
System.out.println("in.close() unsuccessful");
}
}
}
publicclass Cleanup {
publicstaticvoid main(String[] args) {
try {
InputFile in = new InputFile("Cleanup.java");
String s;
int i = 1;
while((s = in.getLine()) != null)
System.out.println(""+ i++ + ": " + s);
in.cleanup();
} catch(Exception e) {
System.out.println( "Caught in main, e.printStackTrace()");
e.printStackTrace();
}
}
}
///
运行后输出的前2行是:
1: //: Cleanup.java
2: // Paying attention to exceptions in constructors
3: import java.io.*;
简要说明 InputFile的类包含一个构建器和两个方法cleanup和getLine。构建器要打开一个文件fname,首先要捕获FileNotFoundException类异常。在它的处理代码中再掷出这个异常(throw e;)。在更高的控制器中试图关闭文件,并捕捉关闭失败的异常IOException。cleanup()关闭文件,getLine()读文件的一行到字符串,它们都用了异常处理机制。Cleanup是主类,在main()中首先创建一个InputFile类对象,因为它的构建器声明时用了异常规范,所以必须用try-catch结构来捕获异常。
4 创建自己的异常类
虽然Java类库提供了十分丰富的异常类型,能够满足绝大多数编程需要。但是,在开发较大的程序时,也有可能需要建立自己的异常类。要创建自己的异常类,必须从一个现有的异常类型继承——最好在含义上与新异常近似。创建一个异常相当简单,只要按如下格式写两个构建器就行:
class MyException extends Exception {
public MyException() {}
public MyException(String msg) {
super(msg);
}
}
这里的关键是“extends Exception”,它的意思是:除包括一个Exception的全部含义以外,还有更多的含义。增加的代码数量非常少——实际只添加了两个构建器,对MyException的创建方式进行了定义。请记住,假如我们不明确调用一个基础类构建器,编译器会自动调用基础类默认构建器。在第二个构建器中,通过使用super关键字,明确调用了带有一个String参数的基础类构建器。
例6 本例程演示建立和应用自己的异常类。
///
//: Inheriting.java
// Inheriting your own exceptions
class MyException extends Exception {
public MyException() {}
public MyException(String msg) {
super(msg);
}
}
publicclass Inheriting {
publicstaticvoid f() throws MyException {
System.out.println(
"Throwing MyException from f()");
thrownew MyException();
}
publicstaticvoid g() throws MyException {
System.out.println(
"Throwing MyException from g()");
thrownew MyException("Originated in g()");
}
publicstaticvoid main(String[] args) {
try {
f();
} catch(MyException e) {
e.printStackTrace();
}
try {
g();
} catch(MyException e) {
e.printStackTrace();
}
}
}
///
输出结果:
Throwing MyException from f()
MyException
at Inheriting.f(Inheriting.java:14)
at Inheriting.main(Inheriting.java:22)
Throwing MyException from g()
MyException: Originated in g()
at Inheriting.g(Inheriting.java:18)
at Inheriting.main(Inheriting.java:27)
创建自己的异常时,还可以采取更多的操作。我们可添加额外的构建器及成员:
class MyException2 extends Exception {
public MyException2() {}
public MyException2(String msg) {
super(msg);
}
public MyException2(String msg, int x) {
super(msg);
i = x;
}
public int val() { return i; }
private int i;
}
本章小结:
-
应用异常控制机制进行异常处理的格式是
try{要监控的代码}
catch(XXXException e) {异常处理代码}
finally {必须执行的代码}
-
不知道有什么异常类好用时可查阅相关包中有哪些XXXException.class文件。而用Exception可捕获任何异常。
-
在方法声明中使用了throws关键字的必须进行异常控制,否则会报编译错误。
-
也可以创建自己的异常类。