在學習《深入理解java虛擬機器》一書中,關於類的初始化一章中提到了一句:靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊中可以賦值,但是不能訪問。
並給出一個樣本:
public class MyClass {static {i = 0; //給變數賦值可以正常通過System.out.println(i); //編譯器提示非法向前引用}static int i = 1;} 看了之後有兩點疑問
1.我們知道類變數和靜態語句塊的初始化順序是按照出現順序的,在執行i = 0的時候,還未執行static int i = 1,為什麼不會報變數未定義的錯。
2.既然i = 0沒有問題,為什麼不可以訪問。
帶著這些問題去看了網上的一些關於向前引用的部落格後,有了一些自己的理解。
看三個例子:
第一個例子:
public class MyClass { void method() { System.out.println(myvar); } String myvar = "var value"; public static void main(String[] args) {new MyClass().method();}}//output:var value 在這個例子中,可以看到產生對象調用method()方法,method方法裡是可以訪問到myvar變數的,這是因為在new出對象時,會首先為成員變數分配儲存空間並初始化。在執行method()方法時,在堆中的MyClass對象中已經有了一個名為myvar的引用並初始化指向了字串“var char”。所以可以訪問這個變數並列印出來。
第二個例子:
public class MyClass { int method() {return n; } int m = method(); int n = 1;}//output://1//0 這個例子,列印n和m的話依次輸出1和0。現在分析對象建立的過程來解釋這樣的結果。new MyClass()執行後,首先會在堆中划出一片地區儲存物件,並且為成員變數賦預設的初始值(類似於類載入過程中的準備階段為靜態變數非final變數賦預設值),然後再按照成員變數的出現順序進行初始化(這個時候可以運行java代碼),等所有成員變數初始化完成之後調用構造方法完成對象的建立。
現在根據以上一段的分析,m和n一開始都是0,然後開始初始化,執行m=method(); 這個時候n還未初始化還是預設值0,所以傳回值賦給m,m是0。再來初始化n,執行int n = 1後,n由0變成1。所以對象建立完成之後m=0,n=1。輸出結果也就不難解釋了。
第三個例子:
public class MyClass { int m = n; //非法向前引用 { n = 0; System.out.println(n); //非法向前引用 } int n = 1; }
在編譯這段代碼時,編譯期無法通過,提示非法向前引用。似乎這個例子中,int m=n和上個例子中int m = method(); method() {return n;}n的效果是一樣的。其實,如果編譯能夠通過,確實效果是一樣的。按照我們的分析,int m = n和代碼塊中println(n)是沒有問題的,可以給m賦還未初始化n的預設值0以及列印n的預設值0,但是java卻不允許我們這樣做。仔細想想就會明白,這是java出於安全性考慮的:如果欄位會進行初始化,一定要防止在初始化前程式中訪問預設值(可以賦值但不可訪問),這會導致很多意想不到的麻煩。
在構造器多態行為一文http://blog.csdn.net/xdugucc/article/details/78220518中,我們已經見過這種做法帶來的問題。所以編譯期會對變數還未完全初始化卻在前面被引用的現象進行檢查並規避,但是如果像第二個例子中的int m = method(); method() {return n;}的現象,編譯器卻無能為力,因為在編譯期它無法知道是什麼樣的值賦值給了m。所以編譯器並不會報錯,但是我們要避免這樣的現象發生。
現在再來看本文一開始的例子就很清晰了,雖然舉的三個例子是基於對象的建立過程,而一開始的例子是基於類載入的過程,但是意思確實相通的。就是對於有初始值的變數,要避免在初始化之前訪問變數的預設值。但是賦值確是可以的,因為最終變數還是會進行初始化。