盡管在一個方法聲明中看到一個throws子句是很常見的,但是在構(gòu)造器的聲明中看到一個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方法調(diào)用了Reluctant構(gòu)造器,它將拋出一個異常。你可能期望catch子句能夠捕獲這個異常,并且打印I told you so。湊近仔細看看這個程序就會發(fā)現(xiàn),Reluctant實例還包含第二個內(nèi)部實例,它的構(gòu)造器也會拋出一個異常。無論拋出哪一個異常,看起來main中的catch子句都應該捕獲它,因此預測該程序?qū)⒋蛴 told you應該是一個安全的賭注。但是當你嘗試著去運行它時,就會發(fā)現(xiàn)它壓根沒有去做這類的事情:它拋出了StackOverflowError異常,為什么呢?
與大多數(shù)拋出StackOverflowError異常的程序一樣,本程序也包含了一個無限遞歸。當你調(diào)用一個構(gòu)造器時,實例變量的初始化操作將先于構(gòu)造器的程序體而運行[JLS 12.5]。在本謎題中, internalInstance變量的初始化操作遞歸調(diào)用了構(gòu)造器,而該構(gòu)造器通過再次調(diào)用Reluctant構(gòu)造器而初始化該變量自己的internalInstance域,如此無限遞歸下去。這些遞歸調(diào)用在構(gòu)造器程序體獲得執(zhí)行機會之前就會拋出StackOverflowError異常,因為StackOverflowError是Error的子類型而不是Exception的子類型,所以catch子句無法捕獲它。
對于一個對象包含與它自己類型相同的實例的情況,并不少見。例如,鏈接列表節(jié)點、樹節(jié)點和圖節(jié)點都屬于這種情況。你必須非常小心地初始化這樣的包含實例,以避免StackOverflowError異常。
至于本謎題名義上的題目:聲明將拋出異常的構(gòu)造器,你需要注意,構(gòu)造器必須聲明其實例初始化操作會拋出的所有被檢查異常。下面這個展示了常見的“服務提供商”模式的程序,將不能編譯,因為它違反了這條規(guī)則:
public class Car {
private static Class engineClass = ...;
private Engine engine =
(Engine)enginClass.newInstance();
public Car(){ }
}
盡管其構(gòu)造器沒有任何程序體,但是它將拋出兩個被檢查異常,InstantiationException和IllegalAccessException。它們是Class.Instance拋出的,該方法是在初始化engine域的時候被調(diào)用的。訂正該程序的方式是創(chuàng)建一個私有的、靜態(tài)的助手方法,它負責計算域的初始值,并恰當?shù)靥幚懋惓!T诒景钢?,我們假設(shè)選擇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) {
throw new AssertionError(e);
}
}
public Car(){ }
}
總之,實例初始化操作是先于構(gòu)造器的程序體而運行的。實例初始化操作拋出的任何異常都會傳播給構(gòu)造器。如果初始化操作拋出的是被檢查異常,那么構(gòu)造器必須聲明也會拋出這些異常,但是應該避免這樣做,因為它會造成混亂。最后,對于我們所設(shè)計的類,如果其實例包含同樣屬于這個類的其他實例,那么對這種無限遞歸要格外當心。
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方法調(diào)用了Reluctant構(gòu)造器,它將拋出一個異常。你可能期望catch子句能夠捕獲這個異常,并且打印I told you so。湊近仔細看看這個程序就會發(fā)現(xiàn),Reluctant實例還包含第二個內(nèi)部實例,它的構(gòu)造器也會拋出一個異常。無論拋出哪一個異常,看起來main中的catch子句都應該捕獲它,因此預測該程序?qū)⒋蛴 told you應該是一個安全的賭注。但是當你嘗試著去運行它時,就會發(fā)現(xiàn)它壓根沒有去做這類的事情:它拋出了StackOverflowError異常,為什么呢?
與大多數(shù)拋出StackOverflowError異常的程序一樣,本程序也包含了一個無限遞歸。當你調(diào)用一個構(gòu)造器時,實例變量的初始化操作將先于構(gòu)造器的程序體而運行[JLS 12.5]。在本謎題中, internalInstance變量的初始化操作遞歸調(diào)用了構(gòu)造器,而該構(gòu)造器通過再次調(diào)用Reluctant構(gòu)造器而初始化該變量自己的internalInstance域,如此無限遞歸下去。這些遞歸調(diào)用在構(gòu)造器程序體獲得執(zhí)行機會之前就會拋出StackOverflowError異常,因為StackOverflowError是Error的子類型而不是Exception的子類型,所以catch子句無法捕獲它。
對于一個對象包含與它自己類型相同的實例的情況,并不少見。例如,鏈接列表節(jié)點、樹節(jié)點和圖節(jié)點都屬于這種情況。你必須非常小心地初始化這樣的包含實例,以避免StackOverflowError異常。
至于本謎題名義上的題目:聲明將拋出異常的構(gòu)造器,你需要注意,構(gòu)造器必須聲明其實例初始化操作會拋出的所有被檢查異常。下面這個展示了常見的“服務提供商”模式的程序,將不能編譯,因為它違反了這條規(guī)則:
public class Car {
private static Class engineClass = ...;
private Engine engine =
(Engine)enginClass.newInstance();
public Car(){ }
}
盡管其構(gòu)造器沒有任何程序體,但是它將拋出兩個被檢查異常,InstantiationException和IllegalAccessException。它們是Class.Instance拋出的,該方法是在初始化engine域的時候被調(diào)用的。訂正該程序的方式是創(chuàng)建一個私有的、靜態(tài)的助手方法,它負責計算域的初始值,并恰當?shù)靥幚懋惓!T诒景钢?,我們假設(shè)選擇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) {
throw new AssertionError(e);
}
}
public Car(){ }
}
總之,實例初始化操作是先于構(gòu)造器的程序體而運行的。實例初始化操作拋出的任何異常都會傳播給構(gòu)造器。如果初始化操作拋出的是被檢查異常,那么構(gòu)造器必須聲明也會拋出這些異常,但是應該避免這樣做,因為它會造成混亂。最后,對于我們所設(shè)計的類,如果其實例包含同樣屬于這個類的其他實例,那么對這種無限遞歸要格外當心。