【深入Java虛擬機器】之四:類初始化
類初始化是類載入過程的最後一個階段,到初始化階段,才真正開始執行類中的Java程式程式碼。虛擬機器規範嚴格規定了有且只有四種情況必須立即對類進行初始化:
- 遇到new、getstatic、putstatic、invokestatic這四條位元組碼指令時,如果類還沒有進行過初始化,則需要先觸發其初始化。生成這四條指令最常見的Java程式碼場景是:使用new關鍵字例項化物件時、讀取或設定一個類的靜態欄位(static)時(被static修飾又被final修飾的,已在編譯期把結果放入常量池的靜態欄位除外)、以及呼叫一個類的靜態方法時。
- 使用Java.lang.refect包的方法對類進行反射呼叫時,如果類還沒有進行過初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。
- 當虛擬機器啟動時,使用者需要指定一個要執行的主類,虛擬機器會先執行該主類。
1、通過子類引用父類中的靜態欄位,這時對子類的引用為被動引用,因此不會初始化子類,只會初始化父類
class Father{ public static int m = 33; static{ System.out.println("父類被初始化"); } } class Child extends Father{ static{ System.out.println("子類被初始化"); } } public class StaticTest{ public static void main(String[] args){ System.out.println(Child.m); } }
執行後輸出的結果如下:
父類被初始化
33
對於靜態欄位,只有直接定義這個欄位的類才會被初始化,因此,通過其子類來引用父類中定義的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化。
2、常量在編譯階段會存入呼叫它的類的常量池中,本質上沒有直接引用到定義該常量的類,因此不會觸發定義常量的類的初始化
class Const{ public static final String NAME = “我是常量”; static{ System.out.println(“初始化Const類”); } } public class FinalTest{ public static void main(String[] args){ System.out.println(Const.NAME); } }
執行後輸出的結果如下:
我是常量
雖然程式中引用了const類的常量NAME,但是在編譯階段將此常量的值“我是常量”儲存到了呼叫它的類FinalTest的常量池中,對常量Const.NAME的引用實際上轉化為了FinalTest類對自身常量池的引用。也就是說,實際上FinalTest的Class檔案之中並沒有Const類的符號引用入口,這兩個類在編譯成Class檔案後就不存在任何聯絡了。
3、通過陣列定義來引用類,不會觸發類的初始化
class Const{
static{
System.out.println(“初始化Const類”);
}
}
public class ArrayTest{
public static void main(String[] args){
Const[] con = new Const[5];
}
}
執行後不輸出任何資訊,說明Const類並沒有被初始化。
但這段程式碼裡觸發了另一個名為“LLConst”的類的初始化,它是一個由虛擬機器自動生成的、直接繼承於java.lang.Object的子類,建立動作由位元組碼指令newarray觸發,很明顯,這是一個對陣列引用型別的初初始化,而該陣列中的元素僅僅包含一個對Const類的引用,並沒有對其進行初始化。如果我們加入對con陣列中各個Const類元素的例項化程式碼,便會觸發Const類的初始化,如下:
class Const{
static{
System.out.println("初始化Const類");
}
}
public class ArrayTest{
public static void main(String[] args){
Const[] con = new Const[5];
for(Const a:con)
a = new Const();
}
}
這樣便會得到如下輸出結果:
初始化Const類
根據四條規則的第一條,這裡的new觸發了Const類。
最後看一下介面的初始化過程與類初始化過程的不同。
介面也有初始化過程,上面的程式碼中我們都是用靜態語句塊來輸出初始化資訊的,而在介面中不能使用“static{}”語句塊,但編譯器仍然會為介面生成<clinit>類構造器,用於初始化介面中定義的成員變數(實際上是static final修飾的全域性常量)。
二者在初始化時最主要的區別是:當一個類在初始化時,要求其父類全部已經初始化過了,但是一個介面在初始化時,並不要求其父介面全部都完成了初始化,只有在真正使用到父介面的時候(如引用介面中定義的常量),才會初始化該父介面。這點也與類初始化的情況很不同,回過頭來看第2個例子就知道,呼叫類中的static final常量時並不會 觸發該類的初始化,但是呼叫介面中的static final常量時便會觸發該介面的初始化。