【java解惑】類和實例的初始化循環問題


    如下所示代碼:

public class Example049 {

	private int overtime;
	public static Example049 INSTANCE = new Example049();//1
	private static int CURRENT_YEAR = Calendar.getInstance().get(
			Calendar.YEAR);//2

	private Example049() {
		overtime = CURRENT_YEAR - 1970;
	}

	public int getOverTime() {
		return overtime;
	}

	public static void main(String[] args) {
		System.out.println(INSTANCE.getOverTime());
	}
}

    結果說明:

    輸出結果是-1970,如果將1和2調換位置,則輸出結果爲當前年減去1970的差--45。

    

    代碼分析:

    該程序所遇到的問題是由類初始化順序中的循環而引起的。類的初始化是由虛擬機對其 main 方法的調用而觸發的。 首先,其靜態域被設置爲缺省值,其中 INSTANCE 域被設置爲 null,CURRENT_YEAR 被設置爲 0,overtime被設置爲0。接下來,靜態域初始器按照其出現的順序執行初始化。 靜態域INSTANCE的值是通過調用構造器而計算出來的。這個構造器會用一個涉及靜態域 CURRENT_YEAR 的表達式來初始化 overtime。通常,讀取一個靜態域是會引起一個類被初始化的事件之一,但是我們已經在初始化類了,遞歸的初始化嘗試會直接被忽略掉在靜態域被初始化之前,存在着讀取它的值的可能,而此時該靜態域包含的還只是其所屬類型的缺省值。因此,CURRENT_YEAR 的值仍舊是其缺省值 0,這就是爲什麼結果是-1970了。

    由類初始化中的循環所引發的問題是難以診斷的,但是一旦被診斷到,通常是很容易訂正的。要想訂正一個類初始化循環,需要重新對靜態域的初始器進行排序,使得每一個初始器都出現在任何依賴於它的初始器之前。將2和1調換位置,靜態域CURRENT_YEAR會先被初始化,再初始INSTANCE 的時候,CURRENT_YEAR已經有正確的值了,所以輸出結果正確。

     某些通用的設計模式本質上就是初始化循環的, 特別是本題展示的單例模式( Singleton)和服務提供者框架( Service Provider Framework)。類型安全的枚舉模式(Typesafe Enum pattern)也會引起類初始化的循環。

    總之,要當心類初始化循環。最簡單的循環只涉及到一個單一的類,但是它們也可能涉及多個類。 類初始化循環也並非總是壞事,但是它們可能會導致在靜態域被初始化之前就調用構造器。靜態域, 甚至是 final 類型的靜態域,可能會在它們被初始化之前,被讀走其缺省值。

    

    再看如下的代碼片段:

//文件1
public class Example051Parent {

	protected int x;

	Example051Parent(int x) {
		this.x = x;
		output();
	}

	protected void output() {
		System.out.println(x);
	}
}
//文件2
public class Example051Sub extends Example051Parent {
	String f;

	Example051Sub(int x, String f) {
		super(x);
		this.f = f;
	}

	@Override
	protected void output() {
		System.out.println(x + "--" + f);
	}

	public static void main(String[] args) {
		new Example051Sub(1, "ape_it");
	}
}

    上述代碼片段的輸出爲1--null。這個代碼片段和第一個代碼片段不同的是:字符串f是實例域,不是靜態域。在一個實例域被賦值之前,存在着取用其值的可能,而此時它包含的仍舊是其所屬類型的缺省值,這一點和靜態域(類域)是相同的。在這兩個示例中,都會產生初始化循環,第一個是類的初始化循環,第二個是實例初始化循環。但是,需要注意的不同是,循環的類初始化是無法避免的災難,但是循環的實例初始化總是可以且總是應該避免的。

    無論何時,只要一個構造器調用了一個已經被其子類覆寫了的方法,那麼該問題就會出現,因爲以這種方式被調用的方法總在實例被初始化之前執行。要想避免這個問題,就千萬不要在構造器中調用可覆寫的方法 直接調用或間接調用都不行。這項禁令應該擴展至實例初始器和僞構造器(readObject 與 clone,這些方法之所以被稱爲僞構造器,是因爲它們可以在不調用構造器的情況下創建對象)。總之,在任何情況下,你都務必要記住:不要在構造器中調用可覆 寫的方法。在實例初始化中產生的循環將是致命的。



注:本【java解惑】系列均是博主閱讀《java解惑》原書後將原書上的講解和例子部分改編然後寫成博文進行發佈的。所有例子均親自測試通過並共享在github上。通過這些例子激勵自己惠及他人。同時本系列所有博文會同步發佈在博主個人微信公衆號搜索“愛題猿”或者“ape_it”方便大家閱讀。如果文中有任何侵犯原作者權利的內容請及時告知博主以便及時刪除如果讀者對文中的內容有異議或者問題歡迎通過博客留言或者微信公衆號留言等方式共同探討。

源代碼地址https://github.com/rocwinger/java-disabuse


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章