在Java中如果某个方法可能会抛出检查型异常(比如打开一个文件),那么Java编译器会强制在定义该方法的时候必须声明抛出该异常或者该异常的父类异常,否则不能通过编译,这叫做“异常说明”,其形式如void f() throws TooBig,TooSmall{…}。这是种优雅的做法,它使得调用此方法者能确切知道写什么样的代码可以捕获所有潜在的异常。异常限制主要是指针对在继承中当发生覆盖方法的时候,Java编译器要求子类里的覆盖方法中的定义要么没有异常说明,要么有的话则异常说明里的异常必须包含在基类被覆盖方法的异常说明里列出来的那些异常之中。请看下面的代码:
class Exception1 extends Exception{}
class Exception2 extends Exception{}
class Exception3 extends Exception{}
class A{
void f() throws Exception1, Exception2{}
}
public class B extends A{
//1: void f(){System.out.println(“throw no Exception”);}
//2: void f() throws Exception1{System.out.println(“throw Exception1”);}
//3: void f() throws Exception2{System.out.println(“throw Exception2”);}
//4: void f() throws Exception1, Exception2{
// System.out.println(“throw Exception1”);
// }
//!5: void f() throws Exception3{System.out.println(“throw Exception3”);}
}
这段代码中自定义了三个异常Exception1、Exception2和Exception3,类B继承了类A,A中的方法f()声明抛出两个异常Exception1和Exception2。在B中标记了覆盖方法f()的五种书写形式(它们都被注释掉了)。其中标记1、2、3、4所标记的方法f()的书写都是正确的,它们的定义中要么没有异常说明(标记1),要么异常说明里的异常包含在基类方法的异常说明里列出的那些异常之中(标记2、3、4),但是标记5所标记的方法f()是不正确的,不能通过编译器的编译,因为它声明抛出了一个基类方法中没有声明的异常Exception3。
当然,这只是异常限制很简单的一面,稍后我们将看到更多的异常限制。
[b]1、异常限制的原因[/b]
一个方法的定义中如果有异常说明,那么在调用该方法时需要捕获此方法异常说明中的异常(或父类异常)做相应处理。在Java中的异常捕获处理是通过try-catch语句来实现的,如上文类A中的方法f()处理方式如下:
try{
f();
}catch(Exception1 exception1){//…
}catch(Exception2 exception2){//…
}
所以如果我们编写的代码只是同基类A打交道,去调用A中的f(),那么我们就可以将调用f()的语句放入try块中,并随后跟随两个catch语句分别捕捉Exception1和Exception2,而且我们也只需要两个catch语句(因为A中的f()声明只抛出两个异常)。其形式如下:
public class C{
void g(A a){
try{
a.f();
}catch(Exception1 exception1){//…
}catch(Exception2 exception2){//…
}
}
}
类C中的方法g接受一个类A对象的引用作为参数,并调用类A方法f(),我们只需要有两个catch语句去分别捕捉Exception1和Exception2即可。
现在我们考虑向类C方法g传递的参数不是类A对象的引用而是A的子类对象的引用(这时发生了向上转型),比如是类B对象的引用,那么由于Java的多态性a.f()实际调用的是类B中的方法f()。下面我们来真正的思考为什么Java要对覆盖方法做异常限制:如果说类B中的覆盖方法f()声明抛出了一个基类A中的被覆盖方法f()没有声明抛出的异常Exception3,那么当向类C方法g传递类B对象的引用,然后执行上述代码时,a.f()就可能会抛出异常Exception3,但是随后的异常处理代码(catch语句)中并不能捕获处理异常Exception3,于是程序就失灵了。因此,子类B中的覆盖方法f()绝不能抛出基类A中的被覆盖方法f()没有声明抛出的异常。子类B中的覆盖方法f()要么没有声明抛出异常,要么有的话则声明抛出的异常必须包含在基类被覆盖方法的异常说明里列出的那些异常之中。当然,考虑把上述这种情况换到类实现接口上也是一致的。
综上所述,Java中的异常限制的基础在于继承和Java中方法的后期绑定(正是由于Java中方法的后期绑定带来了多态性),其根本原因在于为了防止当程序中发生向上转型时可能带来的异常处理错误从而导致的程序的失灵和崩溃。
[b]2、理解异常限制的重要性[/b]
强大的异常处理机制是Java 的一大优势,正确、合理地使用Java 异常处理能够使程序更健壮。异常限制是Java的异常处理机制中一个重要的知识点,而Java的异常限制又是基于继承和多态性的,领悟Java异常限制的内在原因有助于我们更好的理解继承和多态。
继承是面向对象语言的基本特征之一,用面向对象语言Java编写的稍复杂的程序都会用到继承,而一个可靠健壮的程序也少不了异常处理代码,因此在我们使用Java过程中必然会遇到异常限制的问题,理解异常限制并能熟练运用异常限制对于我们编写正确的Java程序以及提高编程的效率都是十分有利的。
[b]3、异常限制的详细阐述[/b]
分析了异常限制的原因后我们通过一个例子来阐述异常限制的详细内容(代码中被注释掉的都是不能通过编译的):
class A extends Exception{}
class A1 extends A{}
class A2 extends A{}
abstract class B{
public void B() throws A{}
public void f() throws A{}
public void b1() throws A1,A2{}
public void b2(){}
}
class C extends Exception{}
class A11 extends A1{}
interface D{
public void f() throws C;
}
class BD extends B implements D{
public BD() throws C,A{} //1
//! public void b2() throws A11{} //2
//! public void f() throws C{} //3
public void f(){} //4
public void b1() throws A11{} //5
}
类BD继承了类B,并实现了接口D,因为在子类的构造器中基类的构造器必须首先被调用,所以如果基类的构造器中有异常说明,那么子类构造器也必须有异常说明,并且子类构造器的异常说明里的异常必须包含基类构造器的异常说明里的异常。在上述代码中基类B的构造器声明抛出异常A,因此子类BD的构造器中也必须声明抛出异常A。请看标记1处BD的构造器中的异常说明不仅有A,还有一个基类B的构造器中没有声明的异常C,这样做可以吗?思考一下异常限制的定义,因为异常限制是针对覆盖方法的,而子类中的构造器并没有覆盖基类的构造器,所以这样做显然是可以的,即子类构造器可以声明抛出基类构造器的异常说明里没有的异常。
类BD中的方法b2()不能通过编译(标记2处),思考一下异常限制的原因,这是因为:类BD中的b2()覆盖了基类B中的b2(),基类B中的b2()没有声明抛出任何异常,而BD的b2()却声明抛出异常A11。如果编译器允许这么做的话,那么当我们在调用B中的b2()的时候不需要做任何异常处理(因为它并没有声明抛出任何异常),而当发生方法的后期绑定把它替换成B的子类BD的对象时,这个方法却有可能会抛出异常A11,但实际上我们并没有做任何异常处理,所以这个时候程序就可能会出现问题了。因此,如果基类的原方法没有异常说明那么子类中的覆盖方法也不能有异常说明。
类BD中的方法f()很特殊,它的来源有两个,在基类B和接口D中都有方法f()。基类B中的f()声明抛出了异常C,而接口D中的f()没有声明抛出异常,那么子类BD中的f()的异常说明应该是怎样的呢?从代码中我们可以看到声明其抛出异常C是不能通过编译的(标记3处),而没有异常说明是正确的(标记4处)。我们来分析一下原因:因为类BD中f()的来源有两个,它既覆盖了基类B的f()又实现了接口D中的f(),所以它既要遵守基类B的f()带来的异常限制(不能声明抛出异常C以外的异常),又要遵守接口D中的f()带来的异常限制(不能声明抛出异常)。因此综合起来,它能声明抛出的异常就是基类B的f()声明的异常与接口D中的f()声明的异常的“交集”,在这里是个空集,即BD中的f()不能声明抛出异常。于是,标记4处的代码能通过编译,而标记3处的不能。
最后我们来看类BD中的覆盖方法b1(),基类B中的被覆盖方法b1()声明抛出了异常A1和A2,而BD中的b1()抛出了异常A1的子类异常A11(标记5处),这是允许的,因为如果我们可以捕捉异常A1,那么我们自然也就可以捕捉到它的子类异常A11。因此,子类中的覆盖方法可以声明抛出基类中被覆盖方法声明抛出的异常的子类异常。
[b]4、总结[/b]
根据以上对异常限制的详细阐述,下面我们对异常限制的具体内容做一总结:
(1)如果基类构造器有异常说明,那么子类构造器也必须声明抛出基类构造器中声明的那些异常(或这些异常的父类异常),另外,子类构造器的异常说明中也可以有基类构造器的异常说明中没有的异常。
(2)如果基类的被覆盖方法没有异常说明,那么子类里的覆盖方法也不能有异常说明;如果基类的被覆盖方法有异常说明,那么子类里的覆盖方法中的定义要么没有异常说明,要么有的话则异常说明里的异常必须包含在基类被覆盖方法的异常说明里列出的那些异常之中(或是这些异常的子类异常)。
(3)如果子类不仅继承了一个基类还实现了一个或多个接口,而且该覆盖方法在两个或两个以上的接口(基类)中存在,那么子类中的覆盖方法声明抛出的异常应为存在该方法的那些接口(基类)中的该方法声明抛出的异常的交集。
总之,如果方法被覆盖,要求被覆盖的方法一定不能声明抛出新的异常或比原方法范畴更广的异常。