Java與C++有一個不同之處在于,Java不但有構造函數,還有一個”初始化塊“(Initialization Block)的概念。下面探究一下它的執行順序與可能的用途。
執行順序
首先定義A, B, C三個類用作測試,其中B繼承了A,C又繼承了B,并分別給它們加上靜態初始化塊、非靜態初始化塊和構造函數,里面都是一句簡單的輸出。
主類Main里面也如法炮制。
class A {
static {
System.out.println("Static init A.");
}
{
System.out.println("Instance init A.");
}
A() {
System.out.println("Constructor A.");
}
}
class B extends A {
static {
System.out.println("Static init B.");
}
{
System.out.println("Instance init B.");
}
B() {
System.out.println("Constructor B.");
}
}
class C extends B {
static {
System.out.println("Static init C.");
}
{
System.out.println("Instance init C.");
}
C() {
System.out.println("Constructor C.");
}
}
public class Main {
static {
System.out.println("Static init Main.");
}
{
System.out.println("Instance init Main.");
}
public Main() {
System.out.println("Constructor Main.");
}
public static void main(String[] args) {
C c = new C();
//B b = new B();
}
}
測試代碼
當然這里不使用內部類,因為內部類不能使用靜態的定義;而用靜態內部類就失去了一般性。
那么可以看到,當程序進入了main函數,并創建了一個類C的對象之后,輸出是這樣子的:
Static init Main.
Static init A.
Static init B.
Static init C.
Instance init A.
Constructor A.
Instance init B.
Constructor B.
Instance init C.
Constructor C.
觀察上面的輸出,可以觀察到兩個有趣的現象:
Main類是肯定沒有被實例化過的,但是由于執行main入口函數用到了Main類,于是static初始化塊也被執行了;
所有的靜態初始化塊都優先執行,其次才是非靜態的初始化塊和構造函數,它們的執行順序是:
父類的靜態初始化塊
子類的靜態初始化塊
父類的初始化塊
父類的構造函數
子類的初始化塊
子類的構造函數
那么如果有多個實例化對象,又會不會發生變化呢?于是在第一個C類的對象后面,再實例化一個B類的對象,再觀察輸出:
Static init Main.
Static init A.
Static init B.
Static init C.
Instance init A.
Constructor A.
Instance init B.
Constructor B.
Instance init C.
Constructor C.
Instance init A.
Constructor A.
Instance init B.
Constructor B.
可以發現這輸出跟前面的基本長得一樣對吧?只是在后面多了4行,那是新的B類對象實例化時產生的信息,同樣也是父類A的初始化塊和構造函數先執行,再輪到子類B的初始化塊和構造函數執行;同時還發現,靜態初始化塊的輸出只出現了一次,也就是說每個類的靜態初始化塊都只在第一次實例化該類對象時執行一次。
無論如何,初始化塊和構造函數總在一起執行是件有趣的事情,讓我們反編譯一下看看吧!
查看生成目錄發現已經生成了4個.class文件,分別是A.class, B.class, C.class, Main.class,先看看Main.class的結構(這里重新注釋了new B):
Compiled from "Main.java"public class Main {? public Main();? ? Code:? ? ? 0: aload_0? ? ? 1: invokespecial #1? ? ? ? ? ? ? ? ? // Method java/lang/Object."":()V? ? ? 4: getstatic? ? #2? ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream;? ? ? 7: ldc? ? ? ? ? #3? ? ? ? ? ? ? ? ? // String Instance init Main.? ? ? 9: invokevirtual #4? ? ? ? ? ? ? ? ? // Method java/io/PrintStream.println:(Ljava/lang/String;)V? ? ? 12: getstatic? ? #2? ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream;? ? ? 15: ldc? ? ? ? ? #5? ? ? ? ? ? ? ? ? // String Constructor Main.? ? ? 17: invokevirtual #4? ? ? ? ? ? ? ? ? // Method java/io/PrintStream.println:(Ljava/lang/String;)V? ? ? 20: return? public static void main(java.lang.String[]);? ? Code:? ? ? 0: new? ? ? ? ? #6? ? ? ? ? ? ? ? ? // class C? ? ? 3: dup? ? ? 4: invokespecial #7? ? ? ? ? ? ? ? ? // Method C."":()V
7: astore_1
8: return
static {};
Code:
0: getstatic? ? #2? ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc? ? ? ? ? #8? ? ? ? ? ? ? ? ? // String Static init Main.
5: invokevirtual #4? ? ? ? ? ? ? ? ? // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
Main.class的反編譯結果
可以看到整個Main類被分成三個部分,static {}部分很顯然,就是我們的static初始化塊,在里面調用了println并輸出了String“Static init Main.”;而main入口函數也很清晰,首先新實例化了一個類C的對象,然后調用了類C的構造函數,最后返回;而上面public Main();的部分就很有意思了,這是類Main的構造函數,但我們看到里面調用了兩次println,分別輸出了String“Instance init Main.”和String“Constructor Main.”。難道初始化塊和構造函數被合并到一起了?
我們再看看C類的反編譯結果吧:
Compiled from "Main.java"class C extends B {? C();? ? Code:? ? ? 0: aload_0? ? ? 1: invokespecial #1? ? ? ? ? ? ? ? ? // Method B."":()V
4: getstatic? ? #2? ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc? ? ? ? ? #3? ? ? ? ? ? ? ? ? // String Instance init C.
9: invokevirtual #4? ? ? ? ? ? ? ? ? // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: getstatic? ? #2? ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc? ? ? ? ? #5? ? ? ? ? ? ? ? ? // String Constructor C.
17: invokevirtual #4? ? ? ? ? ? ? ? ? // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: return
static {};
Code:
0: getstatic? ? #2? ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc? ? ? ? ? #6? ? ? ? ? ? ? ? ? // String Static init C.
5: invokevirtual #4? ? ? ? ? ? ? ? ? // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
C.class的反編譯結果
靜態初始化塊仍然單獨分出一部分,輸出了我們的調試語句。而另一部分,仍然還是類C的構造函數C();,可以看到它先調用了父類B的構造函數,接著輸出了我們初始化塊中的語句,然后才輸出我們寫在構造函數中的語句,最后返回。多次試驗也都是如此。于是我們能夠推斷:初始化塊的代碼是被加入到子類構造函數的前面,父類初始化的后面了。
可能的用途:
既然執行順序和大概原理都摸清了,那么就要探討一下初始化塊的可能的用途。
靜態初始化塊
1. ?用于初始化靜態成員變量
比如給類C增加一個靜態成員變量sub,我們在static塊里面給它賦值為5:
class C extends B {
static public int a;
static {
a = 5;
System.out.println("Static init C.");
}
......
}
main函數里輸出這個靜態變量C.sub:
publicstaticvoidmain(String[] args) {System.out.println("Value of C.sub: " +C.sub);}
Static init Main.
Static init A.
Static init B.
Static init C.
Value of C.sub:?
符合類被第一次加載時執行靜態初始化塊的結論,且C.sub被正確賦值為5并輸出了出來。
但是乍一看似乎沒有什么用,因為靜態成員變量在定義時就可以順便賦值了。因此在賦值方面有點雞肋。
2. ?執行初始化代碼
比如可以記錄第一次訪問類的日志,或方便單例模式的初始化等。對于單例模式,可以先用static塊初始化一些可能還被其他類訪問的基礎參數,等到真正需要加載大量資源的時候(getInstance)再構造單體,在構造函數中加載資源。
非靜態初始化塊
這個就沒什么好說的了,基本跟構造函數一個功能,但比構造函數先執行。最常見的用法應該還是代碼復用,即多個重載構造函數都有若干段相同的代碼,那么可以把這些重復的代碼拉出來放到初始化塊中,但仍然要注意它的執行順序,對順序有嚴格要求的初始化代碼就不適合使用了。
總結:
靜態初始化塊的優先級最高,也就是最先執行,并且僅在類第一次被加載時執行;
非靜態初始化塊和構造函數后執行,并且在每次生成對象時執行一次;
非靜態初始化塊的代碼會在類構造函數之前執行。因此若要使用,應當養成把初始化塊寫在構造函數之前的習慣,便于調試;
靜態初始化塊既可以用于初始化靜態成員變量,也可以執行初始化代碼;
非靜態初始化塊可以針對多個重載構造函數進行代碼復用。