JAVA异常谜题40:不情愿的构造器
尽管在一个方法声明中看到一个throws子句是很常见的,但是在构造器的声明中看到一个throws子句就很少见了。下面的程序就有这样的一个声明。那么,它将打印出什么呢?
public class reluctant {
private reluctant internalinstance = new reluctant();
public reluctant() throws exception {
throw new exception('i’m not coming out');
}
public static void main(string[] args) {
try {
reluctant b = new reluctant();
system.out.println('surprise!');
} catch (exception ex) {
system.out.println('i told you so');
}
}
}
main方法调用了reluctant构造器,它将抛出一个异常。你可能期望catch子句能够捕获这个异常,并且打印i told you so。凑近仔细看看这个程序就会发现,reluctant实例还包含第二个内部实例,它的构造器也会抛出一个异常。无论抛出哪一个异常,看起来main中的catch子句都应该捕获它,因此预测该程序将打印i told you应该是一个安全的赌注。但是当你尝试着去运行它时,就会发现它压根没有去做这类的事情:它抛出了stackoverflowerror异常,为什么呢?
与大多数抛出stackoverflowerror异常的程序一样,本程序也包含了一个无限递归。当你调用一个构造器时,实例变量的初始化操作将先于构造器的程序体而运行[jls 12.5]。在本谜题中, internalinstance变量的初始化操作递归调用了构造器,而该构造器通过再次调用reluctant构造器而初始化该变量自己的internalinstance域,如此无限递归下去。这些递归调用在构造器程序体获得执行机会之前就会抛出stackoverflowerror异常,因为stackoverflowerror是error的子类型而不是exception的子类型,所以catch子句无法捕获它。
对于一个对象包含与它自己类型相同的实例的情况,并不少见。例如,链接列表节点、树节点和图节点都属于这种情况。你必须非常小心地初始化这样的包含实例,以避免stackoverflowerror异常。
至于本谜题名义上的题目:声明将抛出异常的构造器,你需要注意,构造器必须声明其实例初始化操作会抛出的所有被检查异常。下面这个展示了常见的“服务提供商”模式的程序,将不能编译,因为它违反了这条规则:
public class car {
private static class engineclass = ...;
private engine engine =
(engine)enginclass.newinstance();
public car(){ }
}
尽管其构造器没有任何程序体,但是它将抛出两个被检查异常,instantiationexception和illegalaccessexception。它们是class.instance抛出的,该方法是在初始化engine域的时候被调用的。订正该程序的最好方式是创建一个私有的、静态的助手方法,它负责计算域的初始值,并恰当地处理异常。在本案中,我们假设选择engineclass所引用的class对象,保证它是可访问的并且是可实例化的。
下面的car版本将可以毫无错误地通过编译:
//fixed - instance initializers don’t throw checked exceptions
public class car {
private static class engineclass = ...;
private engine engine = newengine;
private static engine newengine() {
try {
return (engine)engineclass.newinstance();
} catch (illegalaccessexception e) {
throw new assertionerror(e);
} catch (instantiationexception e) {