前面講過,java虛擬機只有在程序首次主動使用一個類或接口的時候纔會初始化它。只有6種活動被看作是程序對類和接口的主動使用:
1、創建類的實例。例如new語句創建實例,或者通過反射、克隆及序列號手段來創建實例。
2、調用類的靜態方法。
3、訪問某個類或接口的靜態變量或者對該靜態變量賦值。
4、調用java API中某些反射方法,比如調用Class.forName("Worker")方法,加入Worker類還沒有被初始化,那麼forName()方法就會初始化Worker類,然後返回代表這個Worker類的Class實例。forName()方法是java.lang.Class類的靜態方法。
5、初始化一個類的子類。例如對Sub類的初始化,可看作是它對父類Base類的主動使用,因此會先初始化Base類。
6、java虛擬機啓動時被註明爲啓動類的類。例如對於“java Sample”命令,Sample類就是啓動類,java虛擬機會先初始化它。
除上述6種情形外,其它使用java類的方式都被看作是被動使用,都不會導致類的初始化。下面結合具體的例子來解釋類的初始化時機。
1、對於final類型的靜態變量,如果在編譯時期就能計算出變量的取值,那麼這種變量稱爲編譯時常量。java程序中對類的編譯時常量的使用,被看作是對類的被動使用,不會導致類的初始化。例如以下Tester類的靜態變量a就是編譯時常量,java編譯器在編譯的時候就能計算出a的取值爲6。當Sample訪問Tester.a的時候,並沒有導致Tester類的初始化。
public class Tester {
public static final int a = 2*3;//變量a是編譯時常量
//public stataic final int a = (int)(Math.random()*5); //變量a不是編譯時常量
static {
System.out.println("init Tester");
}
}
public class Sample {
public static void main(String[] args) {
System.out.println("a="+Tester.a);
}
}
以上程序的打印結果爲:a=6。
當java編譯器生成Sample類的.class文件的時候,它不會在main()方法的字節碼流中保存一個表示Tester.a的符號引用,而是直接在字節碼流中嵌入常量值6。因此當程序訪問Tester.a的時候,客觀上無需初始化Tester類。
注:當java虛擬機加載並連接Tester類時,不會在方法區內爲它的編譯時常量a分配內存。
2、對於final類型的靜態變量,如果在編譯時不能計算出變量的取值,那麼程序對類的這種變量的使用,被看作是對類的主動使用,會導致類的初始化。例如把以上Tester類做如下修改:
public class Tester {
public static final int a = (int)(Math.random()*10)/10+1;//變量a不是編譯時常量
static {
System.out.println("init Tester");
}
}
由於編譯器不會計算出變量a的取值,因此變量a不是編譯時常量。當Sample訪問Tester.a的時候,java虛擬機會先初始化Tester類,溼的變量a在方法區on更有特定的內存和初始值。此時Sample類的main()方法的打印結果爲:
init Tester
a=1
3、當java虛擬機初始化一個類時,要求它的所有的父類都已經被初始化,但是這條規則並不適用於接口。在初始化一個類時,並不會先初始化它所實現的接口。在初試一個接口的時候,並不會先初始化它的父接口。
4、只有當程序訪問的靜態變量或者靜態方法的卻在當前類或接口中定義時,纔可看作是對類或接口的主動使用。例如以下程序:
class Base {
static int a = 1;
static {
System.out.println("init Base");
}
static void method(){
System.out.println("method of Base");
}
}
class Sub extends Base {
static {
System.out.println("init Sub");
}
}
public class Sample {
public static void main(String[] args) {
System.out.println("a="+Sub.a);
Sub.method();
}
}
以上程序打印結果爲:
init Base
a=1
method of Base
因爲Sub.a的屬性a是定義在父類中,所以會初始化父類,並不會初始化Sub類。等到Sub.method()方法的時候,纔回去初始化Sub類。
5、調用ClassLoader類的loadClass()方法加載一個類,並不是對類的主動使用,不會導致類的初始化,例如以下程序,先用系統類加載器加載ClassA,儘管ClassA被加載,但是並沒有初始化。當程序調用Class類的靜態方法forName("ClassA")方法顯示初始化ClassA時,纔是對ClassA的主動使用,將導致ClassA被初始化,它的靜態代碼塊被執行。
class ClassA{
static {
System.out.println("now init ClassA");
}
}
public class ClassB {
public static void main(String[] args)throws Exception{
ClassLoader loader = ClassLoader.getSystemClassLoader();//獲得系統類加載器
Class objClass = loader.loadClass("ClassA");//加載ClassA
System.out.println("after load ClassA");
System.out.println("before init ClassA");
objClass = Class.forName("ClassA");//初始化ClassA
}
}
打印結果如下:
after load ClassA
before init ClassA
now init ClassA