JAVA異常謎題40:不情愿的構(gòu)造器

字號:

盡管在一個方法聲明中看到一個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è)計的類,如果其實例包含同樣屬于這個類的其他實例,那么對這種無限遞歸要格外當心。