BAT必面:你真以為你會做這道JVM面試題?
有關Java虛擬機器類載入機制相關的文章一搜一大把,筆者這裡也不必再贅述一遍了。
筆者這裡撈出一道code題要各位大佬來把玩把玩,如果你一眼就看出了端倪,那麼恭喜你,你可以下山了:
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; }
問題:請問這段程式的輸出是什麼?
一般對於這類問題,小夥伴們腦海中肯定浮現出這樣的知識點:
Java中賦值順序:
父類的靜態變數賦值
自身的靜態變數賦值
父類成員變數賦值和父類塊賦值
父類建構函式賦值
自身成員變數賦值和自身塊賦值
自身建構函式賦值
按照這個理論輸出是什麼呢?答案輸出: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了。
類的初始化階段需要做的是執行類構造器。
類構造器是編譯器收集所有靜態語句塊和類變數的賦值語句,按語句在原始碼中的順序合併生成類構造器,物件的構造方法是(),類的構造方法是(),可以在堆疊資訊中看到。
因此,先執行第一條靜態變數的賦值語句,即st = new StaticTest (),此時會進行物件的初始化。
物件的初始化是先初始化成員變數,再執行構造方法。因此列印2->設定a為110->執行構造方法(列印3,此時a已經賦值為110,但是b只是設定了預設值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沒值的原因。
最後再考慮到文字順序,結果就顯而易見了。
相信看到這裡,心中大概有個結論了吧。