Java 中類檔案加載是動態的。JVM 指令封裝在 .class 檔案中,加載過程是動態的——即用到時才加載,不用則不加載。「用到」包含兩種方式:一是 new 一個物件時(注意涉及多態時,編譯器會優化,提前加載涉及多態的類,詳見範例 1);二是當一個類的靜態代碼被調用時。
// Zoo.java
abstract class Animal {
Animal() { System.out.println("Animal constructor"); }
}
class Tiger extends Animal {
Tiger() { System.out.println("Tig constructor"); }
}
class Dog extends Animal {
Dog() { System.out.println("Dog Constructor"); }
}
public class Zoo {
private Animal am; // Example 1.1
// private Dog am; // Example 1.2
private Tiger tiger;
Zoo() {
tiger = new Tiger();
am = new Dog();
}
public static void main(String[] args) {
System.out.println("new Zoo before");
Zoo z = new Zoo();
System.out.println("new Zoo after");
}
}
註釋掉 Example 1.1,運行 Example 1.2,結果顯示:當將子類物件賦值給父類引用時,編譯器優化,加載器在 new 子類物件前已加載父類與子類(Example 1.1 結果);不存在多態時,只有在 new Dog() 時才加載 Dog 及父類。無論何種方式,在 new 之前,類確已加載進記憶體。
new 物件與調用類靜態方法時,隱式機制在工作。java.lang.Class.forName(String) 與 java.lang.ClassLoader.loadClass()。forName 有兩種形式:
public static Class> forName(String className) throws ClassNotFoundException
public static Class> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
第二個方法關鍵在於第二個參數 boolean initialize:若設為 false,加載完類後不會執行靜態代碼與靜態初始化,只有 new 物件時才初始化。第三個參數指定類加載器。
查看 java.lang.Class 源碼,兩方法最終調用私有 native 方法 forName0:
private static native Class forName0(String name, boolean init, ClassLoader loader) throws ClassNotFoundException;
調用 Class.forName(name) 實際內部調用:
forName0(name, true, ClassLoader.getCallerClassLoader());
調用 Class.forName(name, initialize, loader) 實際內部調用:
forName0(name, initialize, loader);
範例 2.1:第二個參數為 false 的情況:
// Zoo.java
abstract class Animal {
static { System.out.println("Animal static code block"); }
Animal() { System.out.println("Animal constructor"); }
}
class Tiger extends Animal { Tiger() { System.out.println("Tig constructor"); } }
class Dog extends Animal { Dog() { System.out.println("Dog Constructor"); } }
public class Zoo {
public static void main(String[] args) throws Exception {
System.out.println("new Zoo before");
Zoo z = new Zoo();
Class c = Class.forName("Dog", false, z.getClass().getClassLoader());
System.out.println("initialize before");
Animal dog = (Animal)c.newInstance();
System.out.println("new Zoo after");
}
}
類加載完成後並未立即執行靜態初始化代碼,而是到實例化時才進行靜態初始化。有時說靜態代碼在類第一次加載時執行且僅一次,這針對 new 物件、第一次訪問靜態代碼及第二參數為 true 而言;對動態加載,若 forName 第二參數設 false,則在實例化時才執行靜態初始化。預設第二參數為 true。
範例 2.2:
// Zoo.java
abstract class Animal {
static { System.out.println("Animal static code block"); }
Animal() { System.out.println("Animal constructor"); }
}
class Tiger extends Animal { Tiger() { System.out.println("Tig constructor"); } }
class Dog extends Animal { Dog() { System.out.println("Dog Constructor"); } }
public class Zoo {
public static void main(String[] args) throws Exception {
Class c = Zoo.class;
ClassLoader loader = c.getClassLoader();
System.out.println("loader before");
Class dog = loader.loadClass("Dog");
System.out.println("instance before");
Animal an = (Animal)dog.newInstance();
}
}
loader 完成後並未立即執行靜態代碼,只有 newInstance() 時才執行靜態初始化,這與 forName 第二參數指定為 false 完全一致。每當編譯單元完成就得到一個 .class 檔案,其中包含該類的 Class 物件,JVM 利用此 Class 物件動態加載類。