第九章 通过异常处理错误
Java的基本理念是“结构不佳的代码不能运行”。
基本异常(exceptional condition)是指引发阻止当前方法或作用域继续执行的问题。把异常情形和普通问题相区分很重要,所谓的普通问题是指,在当前环境下能得到足够的信息,总能处理这个错误。而对于异常情形,就不能继续下去了,因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。
当抛出异常后,首先,同Java中其他对象的创建一样,将使用new在堆上创建异常对象。然后,当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是“异常处理程序”,它 的任务是将程序从错误状态中恢复,所以程序能要么换一种方式运行,要么继续运行下去。
if(t == null){
throw new NullPointerException;
}
这就抛出了异常,于是在当前环境下就不必再为这个问题操心了,它将再别的地方得到处理。
异常参数
所有标准异常都有两个构造器:一个是缺省构造器;另外一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器。
throw new NullPointException(“t = null”);
使用new来创建对象,用以表示错误情况,此对象的引用将传给throw。
能够抛出任意类型的Throwable(它是异常类型的根类)对象。通常,对于不同类型的错误,要抛出相应的异常。错误信息可以保存在异常对象内部或者用异常类的名称来按时。上一层环境通过这些信息来决定如何处理异常。(通常,异常对象中仅有的信息就是异常类型,除此之外不包含任何有意义的内容。)
捕获异常
异常处理的好处之一就是,使你得以先再一个地方专注于正在解决的问题,然后再别的地方处理这些代码中的错误。
try块
如果再方法内部抛出了异常(或者再方法内部调用的其他地方抛出了异常),这个方法再抛出异常的过程中结束。要是不希望方法就此结束,可以再方法内设置一个特殊的块来捕获异常。因为再这个块里“尝试”各种(可能产生异常的)方法调用,所以称为try块。
try {
//code that might generate exceptions
}
对于不支持异常处理的程序语言,要相仔细检查错误,就得再每个方法调用的前后加上甚至和错误检查的代码,甚至再每次调用同一方法时也得这么做。有了异常处理机制,可以把所有动作都放在try块里,然后只需再一个地方捕获所有异常。这意外着代码将更任意编写和阅读,因为完成任务的代码没有与错误检查的代码混在一起。
异常处理程序
try {
//code that might generate exceptions
} catch(Type id1){
// Handle exceptions of Type1
} catch(Type id2){
// Handle exceptions of Type2
} catch(Type id3){
// Handle exceptions of Type3
}
每个catch子句(异常处理程序)看起来就像是接受一个且锦接受一个特殊类型的参数的方法。
当异常被抛出时,异常处理机制将负责搜索参数与异常类型相匹配的第一个处理程序。然后进入catch子句执行,此时认为异常得到了处理。一旦catch子句结束,则处理程序的查找过程结束 。
终止与恢复
异常处理理论上有两种基本模型。一种称为“终止模型”,在这种模型中,将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常抛出,就表明错误已无法挽回,也不能回来继续执行。另一种是“恢复模型”。
创建自定义异常
// Inheriting your own exceptions.
import com.bruceeckel.simpletest.*;
class SimpleException extends Exception {}
public class SimpleExceptionDemo { private static Test monitor = new Test(); public void f() throws SimpleException { System.out.println("Throw SimpleException from f()"); throw new SimpleException(); } public static void main(String[] args) { SimpleExceptionDemo sed = new SimpleExceptionDemo(); try { sed.f(); } catch(SimpleException e) { System.err.println("Caught it!"); } monitor.expect(new String[] { "Throw SimpleException from f()", "Caught it!" }); } }
|
通过写System.err而被打印到控制台的标注错误流。通常这比把信息输出到System.out要好,因为System.out也许会被重定向。
也可以为异常类定义一个接受字符串参数的构造器,也可以加入额外的构造器和成员
import com.bruceeckel.simpletest.*;
class MyException2 extends Exception { private int x; public MyException2() {} public MyException2(String msg) { super(msg); } public MyException2(String msg, int x) { super(msg); this.x = x; } public int val() { return x; } public String getMessage() { return "Detail Message: "+ x + " "+ super.getMessage(); } }
public class ExtraFeatures { private static Test monitor = new Test(); public static void f() throws MyException2 { System.out.println("Throwing MyException2 from f()"); throw new MyException2(); } public static void g() throws MyException2 { System.out.println("Throwing MyException2 from g()"); throw new MyException2("Originated in g()"); } public static void h() throws MyException2 { System.out.println("Throwing MyException2 from h()"); throw new MyException2("Originated in h()", 47); } public static void main(String[] args) { try { f(); } catch(MyException2 e) { e.printStackTrace(); } try { g(); } catch(MyException2 e) { e.printStackTrace(); } try { h(); } catch(MyException2 e) { e.printStackTrace(); System.err.println("e.val() = " + e.val()); } monitor.expect(new String[] { "Throwing MyException2 from f()", "MyException2: Detail Message: 0 null", "%% \tat ExtraFeatures.f\\(.*\\)", "%% \tat ExtraFeatures.main\\(.*\\)", "Throwing MyException2 from g()", "MyException2: Detail Message: 0 Originated in g()", "%% \tat ExtraFeatures.g\\(.*\\)", "%% \tat ExtraFeatures.main\\(.*\\)", "Throwing MyException2 from h()", "MyException2: Detail Message: 47 Originated in h()", "%% \tat ExtraFeatures.h\\(.*\\)", "%% \tat ExtraFeatures.main\\(.*\\)", "e.val() = 47" }); } }
|
printStackTrace()方法,将打印“从方法调用处直到异常抛出外”的方法调用序列。
异常说明
异常说明:以礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。异常说明使用了附加的关键字throws,
void f() throws TooBig, TooSmall, DivZero{//...
代码必须与异常说明保持一致,如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:要么处理这个异常,要么就在异常说明中表明方法将产生异常。通过这种自顶向下强制执行的异常说明机制,Java在编译时就可以保证一定水平的异常准确性。
捕获所有的异常
可以异常的一类Exception异常处理程序来捕获所有类型的异常。
catch(Exception e){..}
这将捕获所有异常,所以最好把它放在处理程序的末尾,以防它抢在其他处理程序之前先把异常捕获了。
一些输出异常信息的方法:
String getMessage();
String getLocaliozedMessage();
String toString();
void printStackTrace();
void ptintStackTrace(PrintStream);
void printStackTrace(java.io.PrintWriter);
重新抛出异常
catsh(Exception e){
System.err.println("An exception was thrown");
throw e;
}
重新抛出异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch子句将被忽略。此外,异常点的所有信息都得到保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。
如果只是把当前异常对象重新抛出,那么printStackTrace()方法显示的将好是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要相更新这个信息,可以调用fillInStackTracce()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那么异常对象而建立的。
永远不必为清理前一个异常对象而担心,或者说为异常对象的清理而担心。他们都是用new创在堆上创建的,所以垃圾回收器会自动把他们清理掉。
Java标准异常
Throwable这个Java类被用来表示任何可以作为异常被抛出的类。Throwable对象课分为两个类型:Error用来表述编译时和系统错误;Exception是可以被抛出的基本类型。
RuntimeException(运行时异常)的特例
if( t == null )
throw new NullPointerException();
如果对null引用进行调用,Java会自动抛出NullPointerException异常,所以上述异常是多余的。
运行时异常的类型有很多,他们会自动被Java虚拟机抛出,所以不必在异常说明中把他们列出来。尽管通常不必捕获RuntimeException异常,但还是可以在代码抛出RuntimeException异常。
import com.bruceeckel.simpletest.*;
public class NeverCaught { private static Test monitor = new Test(); static void f() { throw new RuntimeException("From f()"); } static void g() { f(); } public static void main(String[] args) { g(); monitor.expect(new String[] { "Exception in thread \"main\" " + "java.lang.RuntimeException: From f()", " at NeverCaught.f(NeverCaught.java:7)", " at NeverCaught.g(NeverCaught.java:10)", " at NeverCaught.main(NeverCaught.java:13)" }); } }
|
如果RuntimeException没有被捕获而直达main(),那么在程序退出前将调用异常的printStackTrace()方法。
请务必记住:只能在代码中忽略RuntimeException(及器子类)类型的异常,其他类型异常的处理都是由编译器其那个指实施的。究其原因,RuntimeException代表的是编程错误:
1)无法预料的错误。比如你从控制范围之外传递进来的null引用
2)作为程序员,应该在代码中进行检查的错误。(比如对于ArrayIndexOutOfBoundsException,这的注意一下数组的大小了)
使用finally进行清理
无论异常是否被抛出,finally子句总能被执行。
缺憾:异常丢失
Java的异常实现也有缺陷。作为程序出错的标志,决不应该被忽略,但它还是可能被轻易地忽略。在某些特殊的方式使用finally子句,就会发生这种情况。
import com.bruceeckel.simpletest.*;
class VeryImportantException extends Exception { public String toString() { return "A very important exception!"; } }
class HoHumException extends Exception { public String toString() { return "A trivial exception"; } }
public class LostMessage { private static Test monitor = new Test(); void f() throws VeryImportantException { throw new VeryImportantException(); } void dispose() throws HoHumException { throw new HoHumException(); } public static void main(String[] args) throws Exception { LostMessage lm = new LostMessage(); try { lm.f(); } finally { lm.dispose(); }
|
VeryImportantException不见了,它被finally子句里的HoHumException取代。这是相当严重的缺陷。
异常的限制
当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着,当基类使用的代码应用到器派生类对象的时候,一样能够工作,异常也不例外。
构造器
import com.bruceeckel.simpletest.*; import java.io.*;
class InputFile { private BufferedReader in; public InputFile(String fname) throws Exception { try { in = new BufferedReader(new FileReader(fname)); // Other code that might throw exceptions } catch(FileNotFoundException e) { System.err.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.err.println("in.close() unsuccessful"); } throw e; // Rethrow } finally { // Don't close it here!!! } } public String getLine() { String s; try { s = in.readLine(); } catch(IOException e) { throw new RuntimeException("readLine() failed"); } return s; } public void dispose() { try { in.close(); System.out.println("dispose() successful"); } catch(IOException e2) { throw new RuntimeException("in.close() failed"); } } }
public class Cleanup { private static Test monitor = new Test(); public static void main(String[] args) { try { InputFile in = new InputFile("Cleanup.java"); String s; int i = 1; while((s = in.getLine()) != null) ; // Perform line-by-line processing here... in.dispose(); } catch(Exception e) { System.err.println("Caught Exception in main"); e.printStackTrace(); } monitor.expect(new String[] { "dispose() successful" }); } }
|
如果FileReader的构造器失败了,将抛出FileNotFoundException异常,它必须被捕获。对于这个异常,并不需要关闭文件,因为这个文件还没有被打开。而任何其他捕获异常的catch子句必须关闭文件,因为在他们捕获异常之时,文件已经打开了。close()方法也可能抛出异常,所以尽管它已经在另一个Catch子句块里了,还是要再用以才能够try-catch。
由于finally会再每次完成构造器之后都执行以便,由此它实在不该是调用close()关闭文件的地方。
异常匹配
抛出异常的时候,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。
查找的时候并不要求抛出的异常同处理程序所说明的异常完全匹配。派生类的对象也可以匹配器基类的处理程序。
import com.bruceeckel.simpletest.*;
class Annoyance extends Exception {} class Sneeze extends Annoyance {}
public class Human { private static Test monitor = new Test(); public static void main(String[] args) { try { throw new Sneeze(); } catch(Sneeze s) { System.err.println("Caught Sneeze"); } catch(Annoyance a) { System.err.println("Caught Annoyance"); } monitor.expect(new String[] { "Caught Sneeze" }); } }
|
如果将这个catch子句删掉,只留下
try{
throw new Sneeze();
} catch(Annoyance a){
...
}
该程序仍然能运行,因为这次捕获的是Sneeze的基类。换句话说,catch(Annoyance e)会捕获Annoyance以及所有从它派生的异常。如果决定再方法里加上更多派生异常的化,只要客户程序员捕获的是基类异常,那么它们的代码就无需更改。
如果把捕获基类的catch子句放在最前面,一次相把派生类的异常全给“屏蔽”掉,就相这样:
try{throw new Sneeze();}
catch(Annoyance a){..}
catch(Sneeze s){...}
编译器就会发现Sneeze的catch子句永远也得不到执行,由此它会向你报告错误。
总结
改良的错误恢复机制是增强代码健壮性的最强有力的方式之一。对每个程序来说,错误恢复都是值得考量的基本问题,在Java中尤其如此,因为Java的最初目的就是用来建立让别人使用的构件。“要构造健壮的系统,组成系统的每个构件也必须是健壮的”。通过使用异常来提供一致的错误报告模型,Java是构件能把错误信息可靠地通知给客户代码。
Java异常处理的目的就是尽可能用比现在更少的代码,来简化开发大型、可靠程序的过程。并且再开发过程镇南关会更有信心,因为程序中的所有错误都将得到处理。异常这种语言功能并非很难学习,而且为项目带来立杆见影的效果。