1. 程式人生 > >Java類、例項的初始化順序

Java類、例項的初始化順序

今晚是阿里巴巴 2013 校園招聘的杭州站筆試。下午匆忙看了兩張歷年試卷,去現場打了瓶醬油。

題目總體考察點偏基礎,倒數第二題(Java 附加題)比較有趣,考察了 Java 初始化機制的細節,在此摘錄出來。

題目

求如下 java 程式碼的輸出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class T  implements Cloneable{
  public static int k = 0;
  public static T t1 = new T(
"t1");
public static T t2 = new T("t2"); public static int i = print("i"); public static int n = 99; public int j = print("j"); { print("構造快"); } static { print("靜態塊"); } public T(String str) { System.out.println((++k) + ":" + str + " i=" + i + " n=" + n); ++
n; ++ i;
} public static int print(String str){ System.out.println((++k) +":" + str + " i=" + i + " n=" + n); ++n; return ++ i; } public static void main(String[] args){ T t = new T("init"); } }

分析

程式碼主要考察的是類、變數初始化的順序。

一般的,我們很清楚類需要在被例項化之前初始化,而物件的初始化則是執行構造方法中的程式碼。

本題的程式碼顯然沒有這麼簡單了。本題中涉及到了static {…} 和 {…}這種形式的程式碼塊,以及在類的靜態變數中初始化該類的物件這種交錯的邏輯,容易讓人焦躁(類似於密集恐懼症吧=()。實際上,按照類的裝載、連結和初始化邏輯,以及物件初始化的順序來思考,不難得到答案。

程式碼組成

  • 成員變數 2~6 行的變數是 static 的,為類 T 的靜態成員變數,需要在類載入的過程中被執行初始化;第 8 行的int j則為例項成員變數,只再類被例項化的過程中初始化。

  • 程式碼段 9~11 行為例項化的程式碼段,在類被例項化的過程中執行;13~15 行為靜態的程式碼段,在類被載入、初始化的過程中執行。

  • 方法 方法public static int print(String str) 為靜態方法,其實現中牽涉到 k,i,n 三個靜態成員變數,實際上,這個方法是專門用來標記執行順序的方法;T 的構造方法是個例項化方法,在 T 被例項化時呼叫。

  • main 方法 main 方法中例項化了一個 T 的例項。

執行順序分析

在一個物件被使用之前,需要經歷的過程有:類的裝載 -> 連結(驗證 -> 準備 -> 解析) -> 初始化 -> 物件例項化。(詳情參見《Java 類的裝載、連結和初始化》),這裡需要注意的點主要有:

  • 在類連結之後,類初始化之前,實際上類已經可以被例項化了。

    就如此題程式碼中所述,在眾多靜態成員變數被初始化完成之前,已經有兩個例項的初始化了。實際上,此時對類的例項化,除了無法正常使用類的靜態承運變數以外(還沒有保證完全被初始化),JVM 中已經載入了類的記憶體結構佈局,只是沒有執行初始化的過程。比如第 3 行public static T t1 = new T("t1");,在連結過程中,JVM 中已經存在了一個 t1,它的值為 null,還沒有執行new T("t1")。又比如第 5 行的public static int i = print("i");,在沒有執行初始化時,i 的值為 0.

  • 先執行成員變數自身初始化,後執行static {…}{…}程式碼塊中的內容。

    如此策略的意義在於讓程式碼塊能處理成員變數相關的邏輯。如果不使用這種策略,而是相反先執行程式碼塊,那麼在執行程式碼塊的過程中,成員變數並沒有意義,程式碼塊的執行也是多餘。

  • 類例項化的過程中,先執行隱式的構造程式碼,再執行構造方法中的程式碼 這裡隱式的構造程式碼包括了{}程式碼塊中的程式碼,以及例項成員變數宣告中的初始化程式碼,以及父類的對應的程式碼(還好本題中沒有考察到父類這一繼承關係,否則更復雜;))。為何不是先執行顯示的構造方法中的程式碼,再執行隱式的程式碼呢?這也很容易解釋:構造方法中可能就需要使用到例項成員變數,而這時候,我們是期待例項變數能正常使用的。

有了如上的分析,也就能推到出最終的輸出結果了。實際上,這幾個原則都不需要死記硬背,完全能通過理解整個 JVM 的執行過程來梳理出思路的。

答案

1
2
3
4
5
6
7
8
9
10
11
1:j   i=0   n=0
2:構造快   i=1   n=1
3:t1    i=2  n=2
4:j   i=3   n=3
5:構造快   i=4   n=4
6:t2    i=5  n=5
7:i   i=6   n=6
8:靜態塊   i=7   n=99
9:j   i=8   n=100
10:構造快   i=9   n=101
11:init    i=10  n=102

參考: