Java的基本理念是“结构不佳的代码不能远行”。
发现错误的理想时机是在编译阶段,也就是在试图运行程序之前。然而,编译期间并不能找出所有的错误,余下的问题必须在运行期间解决。这就需要错误源能通过某种方式,把适当的信息传递给某个接受者——该接收者将知道如何正确处理这个问题。Java通过异常来提供一致的错误报告模型。
文章目录
概念
“异常”这个词有“我对此感到惊讶”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理;你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。异常的另外一个相当明显的好处是,它往往能够降低错误处理代码的复杂度。如果不使用异常,那么必须检查特定的错误,并在程序的许多地方去处理它。而如果使用了异常,那就不必在方法的调用处进行检查,因为异常处理机制将保证能够捕获一个错误。并且,只需在一个地方处理错误,即所谓的异常处理程序中。这种方法不仅节省代码,而且把“描述在正常执行过程中做的什么事”的代码和“出了问题该怎么办”的代码相分离。
基本异常
异常情形是指阻止当前方法或作用域继续执行的问题。把异常情形与普通问题相区分很重要,所谓的普通问题是指,在当前环境下能够得到足够的信息,总能处理这个错误。而对异常情形就不能继续下去,因为在当前环境下无法获取得必要的信息来解决问题。所能做的就是跳出当前环境,并把问题提交给上一级环境。这就是异常抛出。
当抛出异常后,随之发生的三件事:
- 同Java中的其他对象一样,将使用new在堆栈上创建异常对象。
- 当前执行的路径被终止,并且从当前环境中弹出异常对象的引用。
- 异常处理程序接管程序,并开始寻找一个恰当的地方来接收弹出的异常并继续执行程序。这个恰当的地方就是异常处理程序,它的任务就是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。
异常最重要的方面之一就是如果发生问题,它们将不允许程序沿着其正常的路径继续走下去。将强制程序停止运行,并告诉我们出了什么问题,或者强制程序处理问题,并返回到稳定状态。
异常参数
与使用Java中的其中对象一样,我们总是用new在对上创建异常对象,这与伴随着存储空间的分配和构造器的调用。所有标准异常类都有两个构造器:一个是默认构造器;另外一个是接收字符串作为参数,以便能把相关信息放入异常对象的构造器:
throw new NullPolinterException("t is null");
关键字throw将产生许多有趣的结果:
- 在使用new创建对象后,此对象的引用传递给throw。
- 从效果上看,他就像是从方法返回的,可以简单地把异常处理看成一种不同的返回机制。但不能过分的和方法返回做类比,因为异常返回的“地点”和普通方法调用返回的“地点”完全不同。抛出异常会退出当前作用域。
搏获异常
监控区,是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。
try块
如果在方法内部抛出异常,这个方法将在抛出异常的地方结束。如果不希望就此结束,可以在方法内设置一个特殊的块来搏获异常。因为在这个块里“尝试”各种方法调用,所以称之为块。
try{
// code
}
异常处理程序
抛出的异常需要处理,否则程序无法继续运行。这个“地点”就是异常处理程序,而且针对每个要搏获的异常,得准备相应的处理程序。异常处理程序紧跟在try语块之后,以关键字catch表示。
try{
} catch (Type1 id1 ) {
} catch (Type2 id2) {
}
异常处理i程序必须紧跟在try之后。当异常被抛出时,异常处理机制将负责搜寻catch表明的类型与抛出的异常类型一致的第一个异常处理程序,并进入catch子句,此时就认为异常得到了处理,当catch子句执行结束就认为异常处理结束。
终止和恢复
异常处理程序理论上有两种基本模型。
- 终止模型:Java执行这种模型。将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已无法返回,也不能继续执行。
- 恢复模型:意思是异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。
在实际的程序中,程序员最后写的多数都是倾向“终止模型”的代码,并忽略恢复行为。主要原因是恢复模型可能是它所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置非常通用性代码。
创建自定义异常
要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承。建立新的异常类型最简单的方法就是让编译器为你产生默认的构造器,所以这几乎不用写多少代码。
class SimpleException extends Exception{
}
在前面的异常参数一节我们知道,异常还有一个接收String类型参数的构造器。所以我们也可以为异常类定义一个接收字符串的参数的构造器。
class SimpleException extends Exception{
public SimpleException(){
}
public SimpleException(String msg){
super(msg);
}
}
还可以进一步的定义异常,比如加入额外的构造器和成员。但使用程序包的客户端程序员可能仅仅只是查看一下抛出的异常类型,其他的就不管了,所以对异常所添加的其他功能也许根本用不上。
异常说明
Java鼓励人们把方法可能会抛出的异常告知使用此方法的客户端程序员。Java提供了相应的语法(并强制使用这个语法),使你能以礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。这就是异常说明,它属于方法的一部份,紧跟在形势参数列表之后。
异常说明使用了附加关键字throws,后面接一个所有潜在的异常类型的列表,所以方法定义可能看起来像这样:
void f() Type1,Typ e2,Type3{
}
代码必须于异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:要么处理这个异常;要么就在异常说明中表明次方法将产生的异常。
声明了异常,实际上却不抛出。编译器相信了这个声明,并强制此方法的调用者像真的抛出异常那样使用这个方法。这样做的好处是,为异常先占个位子,以后就可以抛出这个中异常而不用修改已有的代码。
从RuntimeException继承异常,它们可以在没有异常说明的情况下被抛出。
在编译是被强制检查的异常称为检查的异常。
搏获所有的异常
可以只写一个异常处理程序来搏获所有的类型异常。通过搏获异常类型的基类Exception就可以做到这一点(Exception还有基类)。
catch(Exception e){
}
这里捕获了所有的异常,所以它最好写在异常处理程序的末尾,以防它抢在其他处理程序之前先把异常捕获了。Exception的积累是Throwable类,有方法:
- String getMessage()
- String getLocalizedMessage()
获取详细信息,或用本地语言表示的详细信息。 - String toString()返回Throwable的简单描述,要是有详细的信息的话,也会包含在内。
- void printStackTrace()
- void printStackTrace(PrintStrean)
- void printStackTrace(java.io.PrintStrean)
打印Throwable和Throwable的调用栈轨迹。调用栈显示了“把你带到异常抛出地点”的方法调用序列。 - Throwable fillInStackTrace()用于记录Throwable对象的内部记录栈桢的当前状态。
栈轨迹
printStackTrace()方法所提供的信息可以通过getStackTrace()方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一帧。元素0是栈定元素,并且是调用序列中的最后一个方法调用(这个Throwable创建和被抛出的地方)。数组中的最后一个元素和栈底是调用序列中的第一个方法调用。
public class WhoCalled{
static void f(){
try{
thorw new Execption();
} catch(Execption e){
for(StackTraceElmement ste : e.getStackTrace()){
System.out.println(ste.getMethoName());
}
}
}
static void g(){
f();
}
static void h(){
g();
}
public static void mian(String[] arg){
f();
System.out.println("=============");
g();
System.out.println("=============");
h();
}
}
//log
f
main
=============
f
g
main
=============
f
g
h
main
重新抛出异常
有时候希望把刚刚捕获的异常重新抛出,尤其在使用Exception捕获所有的异常的时候。既然已经得到了但前异常对象的引用,可以直接把它重新抛出:
catch(Exception e){
throw e ;
}
重抛异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch子句讲被忽略。此外,异常对象的所有信息都将得以保持,所以高一级环境中捕获此异常的处理程序就可以从这个异常中得到所有信息。
如果直接把当前异常对象重新抛出,那么printStacktrace()方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想跟新这个信息(不显示原来抛出点的栈信息),可以调用fillInStackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的。
catch(Exception e){
throw e.fillInStackTrace() ;
}
调用fillInStackTrace()方法那一行就会成为异常的新发生地。不会在包含这个方法之前的栈信息。
如果在捕获一个异常后抛出一个其他的新的异常,那么高一级环境中捕获异常处理程序中捕获的是新抛出的异常对原来的异常信息一无所知。
catch(Exception e){
throw new NullPolinterException() ;
}
高一级环境中捕获到NullPolinterException异常后,原来的异常e一无所知。
异常链
常常会想在捕获一个异常后抛出另外一个异常,并且希望把原始异常的信息保存下来,这被称为异常链。在JDK1.4之后,Throwable的子类在构造器中都可以接受一个cause(因由)对象作为参数。这个cause就用来表示原有异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链跟踪到异常最初发生的位置。
Throwable的子类中,只有三种基本异常类提供了带cause参数的构造器。Error,Exception,RuntimeException。如果要把其他异常的链接起来,应该使用initCause()方法而不是构造器。
catch(Exception e){
throw new RuntimeException(e) ;
}
catch(Exception e){
MyException me = new MyException();
me.initCaues(e);
throw me ;
}
Java标准异常
Throwable这个Java类被用来任何可以作为异常被抛出的类。Throwable可以分为两种:
- Error用来表示编译时和系统错误
- Exception是可以被抛出的基类型。
使用finally进行清理
对于一些代码,可能希望无论try块中的异常是否抛出,它们都能得到执行。这通常使用于垃圾回收之外的情况。为了达到这个效果,可以在异常处理程序的后面加上f ianlly子句。
try{
} catch(Exception e){
} fianlly{
}
无论是否抛出异常,fianlly块的语句一定会被执行。
finally的作用
对于没有垃圾回收和析构函数自动调用机制的语言来说,fianlly显得非常重要,但Java有垃圾回收机制,但没有析构函数可调用。那么fianlly在Java中什么情况下会被用到呢?
当要把除内存之外的资源恢复到初始状态就会用到fianlly。例如:打开的文件或网络连接等。我们可以把恢复操作放在当前环境的末尾处关闭(程序正常执行),或者异常处理程序里面(catch子句里面,当异常发生时)但程序可能发生异常,或抛出的异常没有被捕获到,这样就没有办法恢复外部资源。但有了fianlly,不管异常是否被抛出,它的子句都会被执行,所以可以把恢复操作放在fianlly中。
在return中使用fianlly
因为fianlly总是被执行的,所以在一个方法,可以从多个点返回,并且可以保证重要的清理工作仍旧会执行。
public class Test{
public static void main(String[] arg){
System.out.println("main i :"+f(4));
}
private int f(int i){
try{
if(i == 4){
return g();
}
} finally {
System.out.println("finally f");
}
return 0;
}
private int g(){
System.out.println("return g");
return 3 ;
}
}
// log
return g
finally f
main i :3
可见fianlly还是被执行了,并且在return之后。
缺憾:异常丢失
Java的异常实现也有瑕疵。异常作为程序出错的标志,决不应该被忽略。但他还是有可能被轻易的忽略。
class ExceptionOne extends Exception{
pulic String toString(){
return "ExceptionOne";
}
}
class ExceptionTwo extends Exception{
pulic String toString(){
return "ExceptionTwo";
}
}
public class Test{
pulic void f() throws ExceptionOne{
throw new ExceptionOne();
}
pulic void g() throws ExceptionTwo{
throw new ExceptionTwo();
}
public static void main(String[] arg){
try{
try{
f();
} finally{
g();
}
} catch(Exception e){
System.out.println(e);
}
}
}
// log
ExceptionTwo
可见方法f中抛出的异常已经完全看不见了,已经丢失了。
构造器中抛出异常
程序员需要时刻注意,“如果异常发生了,所有东西都能被正确的清理吗?”。多数情况下是安全的,但是涉及到构造器时可能就有问题了。构造器会把对象设置成安全的状态,但如果构造器里面还有其他动作,并可能会抛出异常时,在编写代码时就需要格外注意。比如在构造器里面打开一个文件,这样的动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才能得以清理。但这个文件可能不存在,所以可能会抛出FileNotFoundException。这时候这些清理动作就不能正常工作了。
可能想到使用finally,但fianll不管异常没有没抛出都会执行,即如果没有抛出也会执行fianlly,即没有抛出异常清理动作也发生了,但我们希望的是没有发生异常是用户自己手动调用清理方法清理。或者是,异常抛出了,但文件还没有被打开,这个时候不需要清理动作。
所以在构造器内部不应该使用fianlly来关闭文件。
解决办法是使用套类:
tyr{
// 调用可能会抛出异常的构造器方法
// 能走到下面的代码,说明文件已经打开了成功了,所以一定要关闭。
try{
}fianlly{
// 清理动作
}
} catch(Exception e){
}
这种通用的清理惯用法在构造器不抛出异常时也应该运用,其基本规则是:在创建需要清理的对象之后,立即进入一个try-fianlly语句。
异常匹配
抛出异常的时候,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到匹配的异常处理程序之后,它就认为异常将得到处理,然后就不要继续查找。查找的时候并不要求抛出的异常同异常处理程序中说明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序。
异常限制
当覆盖方法时,只能抛出在基类方法的异常说明里列出的那些异常类型。但一个出现在基类方法的异常说明中的异常,不一定会出现在派生类方法的异常说明里,即基类的方法有异常说明,但在派生类中覆盖了方法,这个方法可以没有异常说明,或某个异常说明。
异常限制对构造器不起作用。即派生类的构造器可以添加基类构造器类没有的异常说明。但由于基类构造器总是会被调用,所以在派生类构造器的异常说明中总是要包含基类构造器中的异常说明。