了解了類加載器的來龍去脈,你將可以讓你的程序具有強大的動態性----在Java虛擬機不重啟的情況下做出具有載入新類的功能;不關閉Java虛擬機的情況下,釋放類所占用的記憶體,記憶體不會因為充滿了同一個類的多個版本而面臨記憶體不足的窘境。
類加載器的功能,就是把類從靜態的硬盤里(.class文件),復制一份放到記憶體之中,並做一些初始化的工作,讓這個類“活起來”,其他人就能夠使用它的功能。類加載器是構成JRE的其中一個重要成員。
自己編寫的類只會在用到的時候才載入,稱為依需求載入;基礎類庫是一次性載入的,稱為預先載入,這是因為基礎類庫里頭大多是Java程序執行時必備的類,所以為了不要老師做浪費時間的I/O動作(讀取文檔系統,然后載入記憶體),預先載入這些類庫會讓Java應用程序在執行時速度稍快一些。依需求載入時,僅僅聲明一個類型是不會被加載的,只有實例化才會加載,其有點是節省記憶體,缺點是當第一次用到該類時,系統需要花一些時間來加載該類。(注:如果一個類有父類,載入它之前會先載入父類,類加載器會依繼承體系最上層的類往下依序載入)
Java提供兩種方法來達成動態行,一種是隱式的,另一種是顯式的。這兩種方式底層用到的機制完全相同,差異只有程序代碼不同。隱式的就是當用到new這個Java關鍵字時,會讓類加載器依需求載入所需的類。顯式的又分為兩種方法:一種是借用java.lang.Class里的forName()方法,另一種則是借用java.lang.ClassLoader里的loadClass()方法。
使用顯式的方法達成動態性,可以在不修改主程序的情況下增加主程序的功能:
public interface Assembly{
public void start();
}
public class Office{
public static void mail(String args[]) throws Exception{
Class c=Class.forName(arg[0]);
Object o=c.newInstance();
Assembly a=(Assembly)o;
A.start();
}
}
public class Word implements Assembly{
public void start(){
System.out.println("Word starts");
}
}
public class Excel implements Assembly{
public void start(){
System.out.println("Excel starts");
}
}
如此以來,我們的主程序Office.java只要編譯之后,往后只要調用java Office Word或java Office Excel就可以動態載入我們需要的類。
實際上,JAVA 2SDK中有兩個forName()方法:
Public static Class forName(String className)
Public static Class forNmae(String name,boolean initialize,ClassLoader loader)
這兩個方法,最后都是連接到原生方法forName0()中,其聲明如下:
Private static native Class forName0(String name,boolean initialize,ClassLoader loader) throws ClassNotFoundException;
只有一個參數的forName()方法:
forName(className,true,ClassLoader.getCallerClassLoader());
而具有三個參數的forName()方法最后會調用:
forName(name,initialize,loader);
public class Office{
public static void mail(String args[]) throws Exception{
Office off=new Office();
Class c=Class.forName(arg[0],true,off.getClass().getClassLoader());
Object o=c.newInstance();
Assembly a=(Assembly)o;
A.start();
}
}
駐:第三個參數用來指定載入類的類加載器,。ClassLoader.getCallerClassLoader()是一個private方法,所以我們無法自行調用,因此必需要自己產生一個Office類的實體,再去取得載入Office類時所使用的類加載器。第二個參數為false時,即使類被載入了,其靜態初始化代碼塊也沒有被調用,而是在實例化時才真正被調用(過去很多書本都說靜態初始化代碼塊是在類第一次載入識被調用的,這其實是不對的)。
另一種用顯式的方法來達成動態性:直接使用類加載器
在Java中,每個類最終的老祖宗都是object,而object里面有一個方法getClass(),就是用來取得某特定類的參考,這個參考,指向的是一個名為Class類(Class.class)的實體,你無法自行產生一個Class類的實體,因為它被聲明為Private,這個Class類的實體是在類(.class)第一次載入內存時就建立的,以后你的程序中產生任何該類的實體,這些實體的內部都會有一個欄位記錄着這個Class類別的所在位置。如下圖:
基本上,我們可以把每個Class類的實體,當作是某個類在內存中的代理人。每次我們需要查詢該類的資料(如其中的field,method等)時,就可以請這個實體幫我們代勞。事實上,Java的反射機制,就大量地應用了Class類。
在Java中,每個類都是有某個類加載器(ClassLoader的實體)來載入,因此,Class類的實體中,都會有欄位記錄載入它的ClassLoader的實體(注意:如果該欄位是null,並不代表它不是由類加載器所載入,而是代表這個類由靴帶式加載器(bootstrap loader,也有人稱root loader)所載入,只不過因為這個加載器不是用Java所編寫,所以邏輯上沒有實體)。如下圖:
從上圖可知,系統里同時存在多個ClassLoader的實體,而且一個類加載器不僅限於只能載入一個類,類加載器可以載入多個類。所以,只要取得Class類實體的參考,就可以利用其getClassLoader()方法取得載入該類的類加載器的參考。最后,取得了ClassLoader的實體,我們就可以調用其loadClass()方法幫我們載入我們想要的類。因此把程序代碼修改如下:
Public class Office{
Public static void main(String args[]) throws Exception{
Office off=new Office();
System.out.println("類准備載入");
ClassLoader loader=off.getClass().getClassLoader();
Class c=loader.loadClass(arg[0]);
System.out.println("類准備實例化");
Object o=c.newInstance();
Object o2=c.newInstance();
}
}
注意:直接使用ClassLoader的loadClass()方法來載入類,只會把類載入內存,並不會調用該類的靜態初始化塊,而必需等到第一次實例化該類別時才會調用。這種情形與使用Class類的forName()方法時,第二個參數傳入false幾乎是相同的結果。
上述代碼還有另一種寫法:
Public class Office{
Public static void main(String args[]) throws Exception{
Class cb=Office.class;
System.out.println("類准備載入");
ClassLoader loader=cb.getClassLoader();
Class c=loader.loadClass(arg[0]);
System.out.println("類准備實例化");
Object o=c.newInstance();
Object o2=c.newInstance();
}
}
直接在程序里使用Office.class,比起產生Office的實體,再用getClass(0取出,這個方法方便的多,也比較節省內存。
自己建立類加載器來載入類別:
在此之前,我們都是使用既有類的載入器來幫我們載入所指定的類。我們還可以利用自己產生的類加載器來載入類,Java本省提供的java.net.URLClassLoader類就可以做到:
Public class Office{
Public static void main(String args[]) throws Exception{
URL u=new URL("file:/d:/my/lib/");//指定搜索類的路徑
URLClassLoaer ucl=new RULClassLoader(new URL[]{u});
Class c=ucl.loadClass(arg[0]);
Assembly asm=(Assembly )c.newInstance();
Asm.start();
}
}
類被那個類加載器載入?
將上述程序修改如下:
Public class Office{
Public static void main(String args[]) throws Exception{
URL u=new URL("file:/d:/my/lib/");//指定搜索類的路徑
URLClassLoaer ucl=new RULClassLoader(new URL[]{u});
Class c=ucl.loadClass(arg[0]);
Assembly asm=(Assembly )c.newInstance();
Asm.start();
URL u1=new URL("file:/d:/my/lib/");//指定搜索類的路徑
URLClassLoaer ucl1=new RULClassLoader(new URL[]{u1});
Class c1=ucl1.loadClass(arg[0]);
Assembly asm1=(Assembly )c1.newInstance();
Asm1.start();
System.out.println(Office.class.getClassLoader());
System.out.println(u.getClass().getClassLoader());
System.out.println(ucl.getClass().getClassLoader());
System.out.println(c.getClassLoader());
System.out.println(asm.getClass().getClassLoader());
System.out.println(u1.getClass().getClassLoader());
System.out.println(ucl1.getClass().getClassLoader());
System.out.println(c1.getClassLoader());
System.out.println(asm1.getClass().getClassLoader());
}
}
執行輸出結果如下:
由圖可知,Office.class由AppClassLoader(又稱SystemLoader,系統加載器)所載入,URL.class與RULClassLoader.class由Bootstrap Loader(非Java編寫所以為null)所載入。而Word.class分別由兩個不同的RULClassLoader載入。至於Assembly.class,本身應該是由AppClassLoader載入,但是由於多態的關系,所指向的類別實體(Word.class)由特定的加載器加載,導致屏幕上的內容是由其所參考類的實體的加載器,所以在執行getClassLoader()的時候,調用的一定是所參考的類的實體的getClassLoader(),要知道interface本身是由哪個加載器載入,你必需使用如下代碼:Assembly.class.getClassLoader()。(注:Assembly.class是有AppClassLoader載入的)
未完待續....