Java的類/例項初始化過程
昨天看到群裡面有人分享了一道題目,我答錯了,於是趁機瞭解了下Java的類/物件初始化過程:

程式A主要考察的是 類例項初始化 。簡單驗證了下,類例項初始化過程如下:
- 父類例項初始化
- 構造塊/變數初始化(按照文字順序執行)
- 建構函式
程式B考察的則是 類初始化 。類初始化的過程如下:
- 父類初始化
- static變數初始化/static塊(按照文字順序執行)
但是我們必須做到 面向介面程式設計,而不是面向實現程式設計(Program to an ‘interface’, not an ‘implementation’) 。
於是就得看看 Java Language Specification 了。其中 類初始化過程 如下:
- 每個類都有一個初始化鎖LC,程序獲取LC(如果沒有獲取到,就一直等待)
- 如果C正在被其他執行緒初始化,釋放LC並等待C初始化完成
- 如果C正在被本執行緒初始化,即 遞迴初始化 ,釋放LC
- 如果C已經被初始化了,釋放LC
- 如果C處於erroneous狀態,釋放LC並丟擲異常NoClassDefFoundError
- 否則,將C標記為正在被本執行緒初始化,釋放LC;然後, 初始化那些final且為基礎型別的類成員變數
- 初始化C的父類SC和各個介面SI_n (按照implements子句中的順序來) ;如果SC或SIn初始化過程中丟擲異常,則獲取LC,將C標記為erroneous,並通知所有執行緒,然後釋放LC,然後再丟擲同樣的異常。
- 從classloader處獲取assertion是否被開啟
- 接下來, 按照文字順序執行類變數初始化和靜態程式碼塊,或介面的欄位初始化,把它們當作是一個個單獨的程式碼塊。
- 如果執行正常,獲取LC,標記C為已初始化,並通知所有執行緒,然後釋放LC
- 否則,如果丟擲了異常E。若E不是Error,則以E為引數建立新的異常ExceptionInInitializerError作為E。如果因為OutOfMemoryError導致無法建立ExceptionInInitializerError,則將OutOfMemoryError作為E。
- 獲取LC,將C標記為erroneous,通知所有等待的執行緒,釋放LC,並丟擲異常E。
可以看到JLS確實規定了父類先初始化(7)、static塊和類變數賦值按照文字順序來(9)。
然後看看 類例項的初始化 :
- 開始呼叫建構函式(給引數賦值)
- 如果這個建構函式在開始就呼叫了其他建構函式,那麼呼叫新的建構函式,並按照本規則處理。如果執行過程中丟擲異常,則整個過程也丟擲同樣的異常。如果正常,繼續。
- 如果建構函式沒有在開始就呼叫其他建構函式。如果本類不是Object,那麼建構函式會隱式或者顯式的 呼叫父類的構造方法 。父類構造方法也依本規則處理。如果執行過程中丟擲異常,則整個過程也丟擲同樣的異常。如果正常,繼續。
- 執行例項初始化和例項變數初始化。順序 按照文字順序 來處理——從左到右、從上到下。如果執行過程中丟擲異常,則整個過程也丟擲同樣的異常。如果正常,繼續。
- 執行剩下的建構函式。如果執行過程中丟擲異常,則整個過程也丟擲同樣的異常。
JLS特意提到,如果子類覆蓋了父類的方法,則在建構函式中, 呼叫的方法也是子類的 。
接下來一個一個看程式碼:
// 程式A // 父類 class Parent { int i = 1; Parent() { System.out.println(i); int x = getValue(); System.out.println(x); } {i = 2;} protected int getValue() {return i;} } // 子類 class Son extends Parent { int j = 1; Son() {j = 2;} protected int getValue() {return j;} } class Test { public static void main(String[] args) { Son son = new Son(); System.out.println(son.getValue()); } }
- 21行,開始呼叫Son的建構函式
- 16行,Son的建構函式開始之前,初始化Parent
- 4行,開始執行i的變數初始化
- 10行,開始執行構造程式碼塊
- 6-8行,開始執行Parent的建構函式。注意,呼叫的getValue方法是子類的,而此時Son.j還沒有被建構函式、變數賦值語句初始化,此時Son.j是0。(輸出2,0)
- 回到16行,繼續執行Son的建構函式
- 22行,列印此時Son.j的值。(輸出2)
所以,程式A的輸出是:
接下來看程式B:
// 程式B public class MagimaTest { public static void main(String[] args) { magimaFunction(); } static MagimaTest st = new MagimaTest(); static { System.out.println("1"); } { System.out.println("2"); } MagimaTest() { System.out.println("3"); System.out.println("a=" + a + ",b=" + b); } public static void magimaFunction() { System.out.println("4"); } int a = 110; static int b = 112; }
- 3行,在執行main之前,需要初始化MagimaTest類。
- 6行,初始化st成員變數,開始初始化st例項。
- 13開始呼叫建構函式,但是開始前,需要處理成員變數初始化
- 10行,執行構造程式碼塊(輸出2)
- 20行,初始化a變數
- 14行,繼續執行建構函式。此時a為110,b尚未初始化,所以是0(輸出3,a=110,b=0)
- 7行,st成員變數初始化結束,執行下一個static程式碼塊(輸出1)
- 21行,繼續初始化下一個static成員變數b
- 4行,呼叫magimaFunction(輸出4)
所以輸出是:
2 3 a=110,b=0 1 4
好了,看完了解析。那麼我再出一個題目吧: 如果將程式B中的MagimaTest.b改為final的,輸出會變化嗎?