在《JVM 之 (12) 類加載機制》一文中詳細闡述了類加載的過程,並舉了幾個例子進行了簡要分析,在文章的最後留了一個懸念給各位,這裏來揭開這個懸念。建議先看完《JVM 之 (12) 類加載機制》這篇再來看這個,印象會比較深入,如若否則,也沒什麼關係~~
下面是程序代碼:
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:app
Java中賦值順序:
1. 父類的靜態變量賦值
2. 自身的靜態變量賦值
3. 父類成員變量賦值和父類塊賦值
4. 父類構造函數賦值
5. 自身成員變量賦值和自身塊賦值
6. 自身構造函數賦值jvm
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了。)
類的初始化階段須要作是執行類構造器(類構造器是編譯器收集全部靜態語句塊和類變量的賦值語句按語句在源碼中的順序合併生成類構造器,對象的構造方法是<init>(),類的構造方法是<clinit>(),能夠在堆棧信息中看到),所以先執行第一條靜態變量的賦值語句即st = new StaticTest (),此時會進行對象的初始化,對象的初始化是先初始化成員變量再執行構造方法,所以設置a爲110->打印2->執行構造方法(打印3,此時a已經賦值爲110,可是b只是設置了默認值0,並未完成賦值動做),等對象的初始化完成後繼續執行以前的類構造器的語句,接下來就不詳細說了,按照語句在源碼中的順序執行便可。
這裏面還牽涉到一個冷知識,就是在嵌套初始化時有一個特別的邏輯。特別是內嵌的這個變量剛好是個靜態成員,並且是本類的實例。
這會致使一個有趣的現象:「實例初始化居然出如今靜態初始化以前」。
其實並無提早,你要知道java記錄初始化與否的時機。
看一個簡化的代碼,把關鍵問題解釋清楚:
spa
public class Test { public static void main(String[] args) { func(); } static Test st = new Test(); static void func(){} }
根據上面的代碼,有如下步驟:.net
詳細看到這裏,心中大概有個結論了吧,若是對於類的加載機制比較模糊的話,能夠參考開篇推薦的博文~ 有問題歡迎留言。
對象