若某个方法产生一个违例,必须保证该违例能被捕获,并获得正确对待。对于Java的违例控制机制,它的一个好处就是允许我们在一个地方将精力集中在要解决的问题上,然后在另一个地方对待来自那个代码内部的错误。
为理解违例是如何捕获的,首先必须掌握“警戒区”的概念。它代表一个特殊的代码区域,有可能产生违例,并在后面跟随用于控制那些违例的代码。
9.2.1 try块
若位于一个方法内部,并“掷”出一个违例(或在这个方法内部调用的另一个方法产生了违例),那个方法就会在违例产生过程中退出。若不想一个throw离开方法,可在那个方法内部设置一个特殊的代码块,用它捕获违例。这就叫作“try块”,因为要在这个地方“尝试”各种方法调用。try块属于一种普通的作用域,用一个try关键字开头:
try {
// 可能产生违例的代码
}
若用一种不支持违例控制的编程语言全面检查错误,必须用设置和错误检测代码将每个方法都包围起来——即便多次调用相同的方法。而在使用了违例控制技术后,可将所有东西都置入一个try块内,在同一地点捕获所有违例。这样便可极大简化我们的代码,并使其更易辨读,因为代码本身要达到的目标再也不会与繁复的错误检查混淆。
9.2.2 违例控制器
当然,生成的违例必须在某个地方中止。这个“地方”便是违例控制器或者违例控制模块。而且针对想捕获的每种违例类型,都必须有一个相应的违例控制器。违例控制器紧接在try块后面,且用catch(捕获)关键字标记。如下所示:
try {
// Code that might generate exceptions
} catch(Type1 id1) {
// Handle exceptions of Type1
} catch(Type2 id2) {
// Handle exceptions of Type2
} catch(Type3 id3) {
// Handle exceptions of Type3
}
// etc...
每个catch从句——即违例控制器——都类似一个小型方法,它需要采用一个(而且只有一个)特定类型的自变量。可在控制器内部使用标识符(id1,id2等等),就象一个普通的方法自变量那样。我们有时也根本不使用标识符,因为违例类型已提供了足够的信息,可有效处理违例。但即使不用,标识符也必须就位。
控制器必须“紧接”在try块后面。若“掷”出一个违例,违例控制机制就会搜寻自变量与违例类型相符的第一个控制器。随后,它会进入那个catch从句,并认为违例已得到控制(一旦catch从句结束,对控制器的搜索也会停止)。只有相符的catch从句才会得到执行;它与switch语句不同,后者在每个case后都需要一个break命令,防止误执行其他语句。
在try块内部,请注意大量不同的方法调用可能生成相同的违例,但只需要一个控制器。
在违例控制理论中,共存在两种基本方法。在“中断”方法中(Java和C++提供了对这种方法的支持),我们假定错误非常关键,没有办法返回违例发生的地方。无论谁只要“掷”出一个违例,就表明没有办法补救错误,而且也不希望再回来。
另一种方法叫作“恢复”。它意味着违例控制器有责任来纠正当前的状况,然后取得出错的方法,假定下一次会成功执行。若使用恢复,意味着在违例得到控制以后仍然想继续执行。在这种情况下,我们的违例更象一个方法调用——我们用它在Java中设置各种各样特殊的环境,产生类似于“恢复”的行为(换言之,此时不是“掷”出一个违例,而是调用一个用于解决问题的方法)。另外,也可以将自己的try块置入一个while循环里,用它不断进入try块,直到结果满意时为止。
从历史的角度看,若程序员使用的操作系统支持可恢复的违例控制,最终都会用到类似于中断的代码,并跳过恢复进程。所以尽管“恢复”表面上十分不错,但在实际应用中却显得困难重重。其中决定性的原因可能是:我们的控制模块必须随时留意是否产生了违例,以及是否包含了由产生位置专用的代码。这便使代码很难编写和维护——大型系统尤其如此,因为违例可能在多个位置产生。
9.2.3 违例规范
在Java中,对那些要调用方法的客户程序员,我们要通知他们可能从自己的方法里“掷”出违例。这是一种有礼貌的做法,只有它才能使客户程序员准确地知道要编写什么代码来捕获所有潜在的违例。当然,若你同时提供了源码,客户程序员甚至能全盘检查代码,找出相应的throw语句。但尽管如此,通常并不随同源码提供库。为解决这个问题,Java提供了一种特殊的语法格式(并强迫我们采用),以便礼貌地告诉客户程序员该方法会“掷”出什么违例,令对方方便地加以控制。这便是我们在这里要讲述的“违例规范”,它属于方法声明的一部分,位于自变量(参数)列表的后面。
违例规范采用了一个额外的关键字:throws;后面跟随全部潜在的违例类型。因此,我们的方法定义看起来应象下面这个样子:
void f() throws tooBig, tooSmall, divZero { //...
若使用下述代码:
void f() [ // ...
它意味着不会从方法里“掷”出违例(除类型为RuntimeException的违例以外,它可能从任何地方掷出——稍后还会详细讲述)。
但不能完全依赖违例规范——假若方法造成了一个违例,但没有对其进行控制,编译器会侦测到这个情况,并告诉我们必须控制违例,或者指出应该从方法里“掷”出一个违例规范。通过坚持从顶部到底部排列违例规范,Java可在编译期保证违例的正确性(注释②)。
②:这是在C++违例控制基础上一个显著的进步,后者除非到运行期,否则不会捕获不符合违例规范的错误。这使得C++的违例控制机制显得用处不大。
我们在这个地方可采取欺骗手段:要求“掷”出一个并没有发生的违例。编译器能理解我们的要求,并强迫使用这个方法的用户当作真的产生了那个违例处理。在实际应用中,可将其作为那个违例的一个“占位符”使用。这样一来,以后可以方便地产生实际的违例,毋需修改现有的代码。
9.2.4 捕获所有违例
我们可创建一个控制器,令其捕获所有类型的违例。具体的做法是捕获基础类违例类型Exception(也存在其他类型的基础违例,但Exception是适用于几乎所有编程活动的基础)。如下所示:
catch(Exception e) {
System.out.println("caught an exception");
}
这段代码能捕获任何违例,所以在实际使用时最好将其置于控制器列表的末尾,防止跟随在后面的任何特殊违例控制器失效。
对于程序员常用的所有违例类来说,由于Exception类是它们的基础,所以我们不会获得关于违例太多的信息,但可调用来自它的基础类Throwable的方法:
String getMessage()
获得详细的消息。
String toString()
返回对Throwable的一段简要说明,其中包括详细的消息(如果有的话)。
void printStackTrace()
void printStackTrace(PrintStream)
打印出Throwable和Throwable的调用堆栈路径。调用堆栈显示出将我们带到违例发生地点的方法调用的顺序。
第一个版本会打印出标准错误,第二个则打印出我们的选择流程。若在Windows下工作,就不能重定向标准错误。因此,我们一般愿意使用第二个版本,并将结果送给System.out;这样一来,输出就可重定向到我们希望的任何路径。
除此以外,我们还可从Throwable的基础类Object(所有对象的基础类型)获得另外一些方法。对于违例控制来说,其中一个可能有用的是getClass(),它的作用是返回一个对象,用它代表这个对象的类。我们可依次用getName()或toString()查询这个Class类的名字。亦可对Class对象进行一些复杂的操作,尽管那些操作在违例控制中是不必要的。本章稍后还会详细讲述Class对象。
下面是一个特殊的例子,它展示了Exception方法的使用(若执行该程序遇到困难,请参考第3章3.1.2小节“赋值”):
//: ExceptionMethods.java
// Demonstrating the Exception Methods
package c09;
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new 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
可以看到,该方法连续提供了大量信息——每类信息都是前一类信息的一个子集。
9.2.5 重新“掷”出违例
在某些情况下,我们想重新掷出刚才产生过的违例,特别是在用Exception捕获所有可能的违例时。由于我们已拥有当前违例的句柄,所以只需简单地重新掷出那个句柄即可。下面是一个例子:
catch(Exception e) {
System.out.println("一个违例已经产生");
throw e;
}
重新“掷”出一个违例导致违例进入更高一级环境的违例控制器中。用于同一个try块的任何更进一步的catch从句仍然会被忽略。此外,与违例对象有关的所有东西都会得到保留,所以用于捕获特定违例类型的更高一级的控制器可以从那个对象里提取出所有信息。
若只是简单地重新掷出当前违例,我们打印出来的、与printStackTrace()内的那个违例有关的信息会与违例的起源地对应,而不是与重新掷出它的地点对应。若想安装新的堆栈跟踪信息,可调用fillInStackTrace(),它会返回一个特殊的违例对象。这个违例的创建过程如下:将当前堆栈的信息填充到原来的违例对象里。下面列出它的形式:
//: Rethrowing.java
// Demonstrating fillInStackTrace()
public class Rethrowing {
public static void f() throws Exception {
System.out.println(
"originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void g() throws Throwable {
try {
f();
} catch(Exception e) {
System.out.println(
"Inside g(), e.printStackTrace()");
e.printStackTrace();
throw e; // 17
// throw e.fillInStackTrace(); // 18
}
}
public static void
main(String[] args) throws Throwable {
try {
g();
} catch(Exception e) {
System.out.println(
"Caught in main, e.printStackTrace()");
e.printStackTrace();
}
}
} ///:~
其中最重要的行号在注释内标记出来。注意第17行没有设为注释行。它的输出结果如下:
originating the exception in f()
Inside g(), e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
因此,违例堆栈路径无论如何都会记住它的真正起点,无论自己被重复“掷”了好几次。
若将第17行标注(变成注释行),而撤消对第18行的标注,就会换用fillInStackTrace(),结果如下:
originating the exception in f()
Inside g(), e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.g(Rethrowing.java:18)
at Rethrowing.main(Rethrowing.java:24)
由于使用的是fillInStackTrace(),第18行成为违例的新起点。
针对g()和main(),Throwable类必须在违例规格中出现,因为fillInStackTrace()会生成一个Throwable对象的句柄。由于Throwable是Exception的一个基础类,所以有可能获得一个能够“掷”出的对象(具有Throwable属性),但却并非一个Exception(违例)。因此,在main()中用于Exception的句柄可能丢失自己的目标。为保证所有东西均井然有序,编译器强制Throwable使用一个违例规范。举个例子来说,下述程序的违例便不会在main()中被捕获到:
//: ThrowOut.java
public class ThrowOut {
public static void
main(String[] args) throws Throwable {
try {
throw new Throwable();
} catch(Exception e) {
System.out.println("Caught in main()");
}
}
} ///:~
也有可能从一个已经捕获的违例重新“掷”出一个不同的违例。但假如这样做,会得到与使用fillInStackTrace()类似的效果:与违例起源地有关的信息会全部丢失,我们留下的是与新的throw有关的信息。如下所示:
//: RethrowNew.java
// Rethrow a different object from the one that
// was caught
public class RethrowNew {
public static void f() throws Exception {
System.out.println(
"originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void main(String[] args) {
try {
f();
} catch(Exception e) {
System.out.println(
"Caught in main, e.printStackTrace()");
e.printStackTrace();
throw new NullPointerException("from main");
}
}
} ///:~
输出如下:
originating the exception in f()
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
at RethrowNew.f(RethrowNew.java:8)
at RethrowNew.main(RethrowNew.java:13)
java.lang.NullPointerException: from main
at RethrowNew.main(RethrowNew.java:18)
最后一个违例只知道自己来自main(),而非来自f()。注意Throwable在任何违例规范中都不是必需的。
永远不必关心如何清除前一个违例,或者与之有关的其他任何违例。它们都属于用new创建的、以内存堆为基础的对象,所以垃圾收集器会自动将其清除。