Java中异常的限制

  在Java中,发现错误的最理想时机是在编译阶段,即运行程序之前。但是,在编译期间,并不能保证所有的错误都被发现,余下的问题必须在运行期间解决。这就需要错误源能通过某种方式,把适当的信息传递给某个接收者——该接收者将知道如何正确处理这个问题。异常处理是Java中唯一正式的错误报告机制,并且通过编译器强制执行。本文略过了异常处理流程等基本知识,直接讨论Java中异常的限制。
  异常的限制,这是指:当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这样的限制可以保证,当派生类对象向上转型为基类对象时,它的某个方法不会抛出基类方法未声明的异常。下面的例1演示了这种限制:
  
例1:

class BaseballException extends Exception {}
class Foul extends BaseballException {}
class Strike extends BaseballException {}

abstract class Inning {
  public Inning() throws BaseballException {}
  public void event() throws BaseballException {
  }
  public abstract void atBat() throws Strike, Foul;
  public void walk() {} 
}

class StormException extends Exception {}
class RainedOut extends StormException {}
class PopFoul extends Foul {}

interface Storm {
  public void event() throws RainedOut;
  public void rainHard() throws RainedOut;
}

public class StormyInning extends Inning implements Storm {
  public StormyInning()
    throws RainedOut, BaseballException {}
  public StormyInning(String s)
    throws Foul, BaseballException {}
  //! void walk() throws PopFoul {} //Compile error
  public void rainHard() throws RainedOut {}
  public void event() {}
  public void atBat() throws PopFoul {}
  public static void main(String[] args) {
    try {
      StormyInning si = new StormyInning();
      si.atBat();
    } catch(PopFoul e) {
      System.out.println("Pop foul");
    } catch(RainedOut e) {
      System.out.println("Rained out");
    } catch(BaseballException e) {
      System.out.println("Generic baseball exception");
    }
    try {
      Inning i = new StormyInning();
      i.atBat();
    } catch(Strike e) {
      System.out.println("Strike");
    } catch(Foul e) {
      System.out.println("Foul");
    } catch(RainedOut e) {
      System.out.println("Rained out");
    } catch(BaseballException e) {
      System.out.println("Generic baseball exception");
    }
  }
}

  我们可以看到,Inning类中的构造器和event()函数都声明抛出了异常,但实际上在方法体中并没有抛出,这是可行并且合理的,因为采用这种方式可以强制用户去捕获可能在覆盖后的event()方法中抛出的异常,这同样适用于抽象方法,如atBat()。
  我们注意观察一下接口Storm,它定义了一个和Inning类中event()方法同名的一个方法,而且参数列表也相同,并且该方法抛出了一个在Inning类中event()方法没有抛出的异常RainOut,那么如果StormyInning既继承了Inning类,又实现了Storm接口,这两个event()方法是不能互相改变彼此的异常接口的。换句话说,StormyInning中的event()方法不能声明抛出Inning类和Storm接口中event()方法声明的任何异常。
  但是,这种异常限制却对构造器不起作用。你可以发现,StormyInning类的构造器中声明抛出了Inning类构造器没有抛出的异常。但是,由于在创建子类对象时会自动调用基类的构造器,所以子类的构造器的异常声明必须包含基类构造器的异常声明。同时我们可以证明,派生类的构造器不能捕获它的基类构造器所抛出的异常,因为在派生类构造器中,super()函数的调用必须放在第一行,这便不能将try-catch语句插入。
  在StormyInning中,walk()函数不能通过编译,这是因为它声明抛出了在Inning.walk()中没有声明抛出的异常。如果编译器允许这么做的话,那么我们在调用Inning.walk()的时候不用做异常处理,但是在我们执行向上转型Inning inning = new StormyInning()后,再调用inning.walk()时,这个方法便可能抛出异常,但我们却并没有捕获。
  覆盖后的event()方法并没有声明抛出任何异常,这说明派生类中的方法可以不声明抛出异常。这样做是可行的,因为这并不会出现上一段所描述的情况。类似的情况出现在StormyInning.atBat()上,它抛出了异常PopFoul,而Inning.atBat()抛出了异常Foul,这么做可行是因为,PopFoul是Foul的子类,能捕获Foul异常的异常处理程序肯定也能捕获到PopFoul异常。
  最后我们注意到main(),如果我们使用的是StormyInning对象的话,那么编译器只会强制要求你捕获StormyInning类所抛出的异常。但是如果我们使用的是通过向上转型得到的基类Inning对象时,那么编译器就会强制你捕获基类Inning所抛出的异常。所有这些限制都是为了能产生更为强壮的异常处理代码。