1.异常的限制
当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这意味着,当基类使用的代码应用到其派生类对象的时候,一样能够工资,异常也不例外。
下面的例子是在编译时施加在异常上面的限制:
public class BaseBallException extends Exception {}
public class Foul extends BaseBallException{}
public class Strike extends BaseBallException{}
public abstract class Inning {
public Inning() throws BaseBallException{}
public void event() throws BaseBallException{}
public abstract void addBat()throws Strike,Foul;
public void walk() {}
}
public class StormException extends Exception{}
public class RainedOut extends StormException{}
public class PopFoul extends Foul{}
public interface Storm {
public void event() throws RainedOut;
public void rainHard() throws RainedOut;
}
public class StromMyInnerings extends Inning implements Storm{
/**
* @throws BaseBallException
*/
public StromMyInnerings() throws RainedOut,BaseBallException {}
public StromMyInnerings(String s)throws RainedOut,BaseBallException{}
/**
* @throws RainedOut
*/
@Override
public void rainHard() throws RainedOut {}
/**
* @throws Strike
*/
@Override
public void addBat() throws PopFoul {}
public void event(){}
public static void main(String[] args) {
StromMyInnerings stromMyInnerings;
try {
stromMyInnerings = new StromMyInnerings();
stromMyInnerings.addBat();
} catch (RainedOut e) {
System.out.println("RainedOut");
} catch (PopFoul e) {
System.out.println("PopFoul");
} catch (BaseBallException e) {
System.out.println("BaseBallException");
}
Inning inning;
try {
inning = new StromMyInnerings();
inning.addBat();
} catch (Strike e) {
System.out.println("Strike");
} catch (Foul e) {
System.out.println("Foul");
} catch (RainedOut e) {
System.out.println("RainedOut");
} catch (BaseBallException e) {
System.out.println("BaseBallException");
}
}
}
在Inning类中,可以看到构造器和event()方法都声明将抛出异常,但实际上没有抛出。这种方式使你能强制用户去捕获可能在覆盖后的event()版本中增加的异常,所以它很合理。这对于抽象方法同样成立,如addBat()。
接口Storm值得注意,因为它包含了一个在Inning中定义的方法event()和一个不在Inning中定义的方法rainHard()。这两个方法都抛出新的异常RainedOut。如果StromMyInnerings类在扩展Inning类的同事又出现了Storm接口,那么Storm里的event()方法就不能改变Inning中的event()方法的异常接口。否则的话,在使用基类的时候就不能判断是否捕获了正常的异常。当然,如果接口里定义的方法不是来自于基类,比如rainHard(),那么此方法抛出什么样的异常都没有问题。
异常限制对构造器不起作用,你会发现StromMyInnerings的构造器可以抛出任何异常,而不必理会基类构造器所抛出的异常。派生类构造器不能捕获基类构造器抛出的异常。
尽管在继承过程中,编译器会对异常说明做强制要求,但异常说明本身并不属于方法类型的一部分,方法类型是由方法的名字与参数的类型组成的。因此,不能基于异常说明来重载方法。此外,一个出现在基类方法的异常,不一定会出现在派生类方法的异常说明里。换句话说,在继承和覆盖的过程中,某个特定方法的“异常说明的接口”不是变大了而是变小了–这恰好和类接口在继承时的情形相反。
2.构造器
如果一个类继承了某个类同时又实现了某个接口,他们有同样的接口方法,但都抛出了不同的捕获性异常,则该子类实现与重写该方法时,则方法声明处不能抛出任何捕获性异常了。如果调用的父类构造器抛出捕获性异常,则子类相应的构造器也只能抛出,不能在构造器里进行捕获。构造器抛出异常时正确的清理方式:比如在构造器中打开了一个文件,清理动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才能得以清理,而不能直接在构造器里的finally块上关闭,因为finally块是不管是否有异常都会关闭,而构造器执行成功能外界需要这个文件流。但如果在文件成功打开后才抛出异常,则需要关闭文件,并向外界抛出异常信息:
public class InputFile {
private BufferedReader in;
public InputFile(String fname) throws Exception{
try {
in = new BufferedReader(new FileReader(fname));
} catch (FileNotFoundException e) {
System.out.println("could not open "+fname);
throw e;
}catch (Exception e) {
try {
in.close();
} catch (IOException e1) {
System.out.println("in.close() unsuccessful");
throw e;
}finally{
}
}
}
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 e) {
throw new RuntimeException("in.close() failed");
}
}
public class CleanUp {
public static void main(String[] args) {
try {
InputFile inputFile = new InputFile("D:/workspace/testjava/src/com/test/CleanUp.java");
try {
String s;
int i = 1;
while ((s = inputFile.getLine()) != null) {
}
} catch (Exception e) {
System.out.println("caught Exception in main");
e.printStackTrace(System.out);
}finally{
inputFile.dispose();
}
} catch (Exception e) {
System.out.println("InputFile construction failed");
}
}
}
InputFile 的构造器接受字符串作为参数,该字符串表示所要打开的文件名。在try块中,会使用此文件名建立了FileReader对象。FileReader对象本身用处并不大,但可以用它来建立BufferedReader对象。注意,使用InputFile 的好处就是把两步操作合二为一。这种通用的清理习惯用法在构造器不抛出任何异常时也应该运用,其基本规则是:在创建需要清理的对象之后,立即进入一个try-finally语句块。
3.异常匹配
抛出异常的时候,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再继续查找。
查找的时候并不要求抛出的异常处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序,例子如下:
public class Annoyance extends Exception{}
public class Sneeze extends Annoyance{}
public class Human {
public static void main(String[] args) {
try {
throw new Sneeze();
} catch (Sneeze e) {
System.out.println("catch Sneeze");
}catch (Annoyance e) {
System.out.println("catch Annoyance");
}
try {
throw new Sneeze();
}catch (Annoyance e) {
System.out.println("catch Annoyance exception");
}
}
}
Sneeze异常会被第一个匹配的catch自居补货,也就是程序里的第一个。然而如果将这个catch字句删掉,只留下Annoyance的catch子句,该横向仍然能正常运行,因为这次补货的是Sneeze的基类。换句话说,catch(Annoyance e)会捕获Annoyance以及所有从它派生的异常。
如果把捕获基类的catch子句放在最前面,以此想把派生类的异常全给“屏蔽”掉,就像这样:
try {
throw new Sneeze();
}catch (Annoyance e) {
//...
} catch (Sneeze e) {
//.....
}
这样编译器就会发现Sneeze的catch子句永远也得不到执行,因此会报错。
4.其他的可选方式
异常处理的一个重要的原则是“只有在你知道如何处理的情况下才捕获异常”。实际上,异常处理的一个重要目标就是把错误处理的代码同错误发生的地点相分离。这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中。这样以来,主干代码就不会与错误处理逻辑混在一起,也更容易理解和维护。
“被检查的异常”可能使问题变得复杂,因为它们强制你在可能还没有准备好处理错误的时候被迫加上cacth子名,即使我们不知道如何处理的情况下,这就导致了异常的隐藏:
try{
//… throw …
}catch(Exception e){//什么都不做
}
把“被检查的异常”转换为“不检查的异常”:当在一个普通方法里调用别的方法时,要考虑到“我不知道该怎样处理这个异常,但是也不能把它‘吞’了,或者只打印一些无用的消息”。JDK1.4的异常链提供了一种新的思路来解决这个问题,可以直接把“被检查的异常”包装进RuntimeException里面:
try{
//… throw 检查异常…
}catch(IDontKnowWhatToDoWithThisCheckedException e){
Throw new RuntimeException(e);
}
如果想把“被检查的异常”这种功能“屏蔽”掉的话,上面是一个好的办法。不用“吞”掉异常,也不必把它放到方法的异常声明里面,而异常链还能保证你不会丢失任何原始异常的信息。你还可以在“知道如何处理的地方”来处理它,也可以其他上层catch里通过 throw e.getCause(); 再次抛出原始的“被检查的异常”。
5.异常使用指南
应该在以下情况使用异常:
1)在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
2)解决问题并且重新调用产生异常的方法。
3)进行少许修补,然后绕过异常产生的地方继续执行。
4)用别的数据进行计算,以代替方法预计会返回的值。
5)把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
6)把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
7)终止程序。
8)进行简化。
9)让类库和程序更安全。
6.总结
异常时java程序设计不可分割的一部分,如果不了解 如何使用它们,那你只能完成很有限的工作。异常处理的有点之一就是它使得你可以在某处集中精力处理你要解决的问题,而在另一边处理你编写的这段代码中产生的错误。