在《Java虛擬機類加載機制》一文中詳細闡述了類加載的過程,並舉了幾個例子進行了簡要分析,在文章的最後留了一個懸念給各位,這裡來揭開這個懸念。建議先看完《Java虛擬機類加載機制》這篇再來看這個,印象會比較深刻,如若不然,也沒什麼關系~~
下面是程序代碼:
package jvm.classload;
public class StaticTest
{
public static void main(String[] args)
{
staticFunction();
}
static StaticTest st = new StaticTest();
static
{
System.out.println("1");
}
{
System.out.println("2");
}
StaticTest()
{
System.out.println("3");
System.out.println("a="+a+",b="+b);
}
public static void staticFunction(){
System.out.println("4");
}
int a=110;
static int b =112;
}
問題是:請問這段程序的輸出是什麼?
這個是我在論壇上看到的一個問題,我覺得比較金典。
一般對於這類問題,小伙伴們腦海中肯定浮現出這樣的knowledge:
Java中賦值順序:
1. 父類的靜態變量賦值
2. 自身的靜態變量賦值
3. 父類成員變量賦值
4. 父類塊賦值
5. 父類構造函數賦值
6. 自身成員變量賦值
7. 自身塊賦值
8. 自身構造函數賦值
ok,按照這個理論輸出是什麼呢?答案輸出:1 4,這樣正確嚒?肯定不正確啦,這裡不是說上面的規則不正確,而是說不能簡單的套用這個規則。
正確的答案是:
2
3
a=110,b=0
1
4
是不是有點不可思議?且聽我一一道來,這裡主要的點之一:實例初始化不一定要在類初始化結束之後才開始初始化。
類的生命周期是:加載->驗證->准備->解析->初始化->使用->卸載,只有在准備階段和初始化階段才會涉及類變量的初始化和賦值,因此只針對這兩個階段進行分析;
類的准備階段需要做是為類變量分配內存並設置默認值,因此類變量st為null、b為0;(需要注意的是如果類變量是final,編譯時javac將會為value生成ConstantValue屬性,在准備階段虛擬機就會根據ConstantValue的設置將變量設置為指定的值,如果這裡這麼定義:static final int b=112,那麼在准備階段b的值就是112,而不再是0了。)
類的初始化階段需要做是執行類構造器(類構造器是編譯器收集所有靜態語句塊和類變量的賦值語句按語句在源碼中的順序合並生成類構造器,對象的構造方法是
這裡面還牽涉到一個冷知識,就是在嵌套初始化時有一個特別的邏輯。特別是內嵌的這個變量恰好是個靜態成員,而且是本類的實例。
這會導致一個有趣的現象:“實例初始化竟然出現在靜態初始化之前”。
其實並沒有提前,你要知道java記錄初始化與否的時機。
看一個簡化的代碼,把關鍵問題解釋清楚:
public class Test {
public static void main(String[] args) {
func();
}
static Test st = new Test();
static void func(){}
}
??根據上面的代碼,有以下步驟:
首先在執行此段代碼時,首先由main方法的調用觸發靜態初始化。 在初始化Test 類的靜態部分時,遇到st這個成員。 但湊巧這個變量引用的是本類的實例。 那麼問題來了,此時靜態初始化過程還沒完成就要初始化實例部分了。是這樣麼? 從人的角度是的。但從java的角度,一旦開始初始化靜態部分,無論是否完成,後續都不會再重新觸發靜態初始化流程了。 因此在實例化st變量時,實際上是把實例初始化嵌入到了靜態初始化流程中,並且在樓主的問題中,嵌入到了靜態初始化的起始位置。這就導致了實例初始化完全至於靜態初始化之前。這也是導致a有值b沒值的原因。 最後再考慮到文本順序,結果就顯而易見了。??詳細看到這裡,心中大概有個結論了吧,如果對於類的加載機制比較模糊的話,可以參考開篇推薦的博文~ 有問題歡迎留言。