1 Java的動態特性
Java的動態特性有兩種,一是隱式的;另一種是顯示的。隱式的(implicit)方法就是當程式設計師用到new 這個Java 關鍵字時,會讓類別載入器依需求載入您所需要的類別,這種方式使用了隱式的(implicit)方法。顯式的方法,又分成兩種方式,一種是藉由java.lang.Class 裡的forName()方法,另一種則
是藉由java.lang.ClassLoader 裡的loadClass()方法。您可以任意選用其中一種方法。
2 隱式的動態特性
在執行java文件時,只有單獨的變量聲明是不會載入相應的類的,只有在用new生成實例時才載入
如示例所示:
public class Main
public static void main(String args[])
{
A a1 = new A() ;
B b1 ;
}
類A和B相同,如下:
public class A
{
public void print(“using A”);
}
編譯後,可用java –verbose:class Main運行,察看輸出結果。可以看到JVM只載入了A,而沒有載入B.
另外,類的載入只在執行到new一個類時,才載入,如果沒有執行到new語句,則不載入。
如://類Office
public class Office
{
public static void main(String[] args)
{
Word myword=null;
Excel myexcel=null;
if (args[0].equals("Word"))
{
myword = new Word();
myword.start();
}
if (args[0].equals("Excel"))
{
myexcel = new Excel();
myexcel.start();
}
}
}
//類Word和Excel基本相同,如下
public class Word
{
public void start()
{
System.out.println("using word");
}
}
在dos命令提示符下,輸入java –verbose Office Excel可以看到JVM只載入Excel類,而不載入Word類。
3 顯示的動態特性
3.1 java.lang.Class裡的forName()方法
在上一個Office示例中,進行如下修改:
一 加入Assembly類
public interface Assembly
{
public void start();
}
二 讓Word和Excel類實現該接口
public class Word implements Assembly
{
public void start()
{
System.out.println("using word");
}
}
三 Office 類如下所示
public class Office
{
public static void main(String[] args) throws Exception
{
java.lang.Class c = java.lang.Class.forName(args[0]);
Object o = c.newInstance();
Assembly a = (Assembly)o;
a.start();
}
}
在命令提示符下輸入java –verbose Office Word 輸出入下:
通過上圖你可以看到,interface 如同class 一般,會由編譯器產生一個獨立的類別檔(.class),當類別載入器載入類別時,如果發現該類別繼承了其他類別,或是實作了其他介面,就會先載入代表該介面的類別檔,也會載入其父類別的類別檔,如果父類別也有其父類別,也會一並優先載入。換句話說,類別載入器會依繼承體系最上層的類別往下依序載入,直到所有的祖先類別都載入了,才輪到自己載入。
下面介紹一下 forName 函數, 如果您親自搜尋Java 2 SDK 說明檔內部對於Class 這個類別的說明,您可以發現其實有兩個forName()方法,一個是只有一個參數的(就是之前程式之中所使用的):
public static Class forName(String className)
另外一個是需要三個參數的:
public static Class forName(String name, boolean initialize,ClassLoader loader)
這兩個方法,最後都是連接到原生方法forName0(),其宣告如下:
private static native Class forName0(String name, boolean initialize, ClassLoader loader)
throws ClassNotFoundException;
只有一個參數的forName()方法,最後叫用的是:
forName0(className, true, ClassLoader.getCallerClassLoader());
而具有三個參數的forName()方法,最後叫用的是:
forName0(name, initialize, loader);
這裡initialize參數指,在載入類之後是否進行初始化,對於該參數的作用可用如下示例察看:
類裡的靜態初始化塊在類第一次被初始化時才被呼叫,且僅呼叫一次。在Word類裡,加入靜態初始化塊
public class Word implements Assembly
{
static
{
System.out.println("word static initialization ");
}
public void start()
{
System.out.println("using word");
}
}
將類Office作如下改變:
public class Office
{
public static void main(String[] args) throws Exception
{
Office off= new Office();
System.out.println("類別准備載入");
java.lang.Class c = java.lang.Class.forName(args[0],true,off.getClass().getClassLoader());
System.out.println("類別准備實體化");
Object o = c.newInstance();
Object o2 = c.newInstance();
}
}
如果第二個參數為true 則輸出入下
如果為false ,則輸出入下:
可見,類裡的靜態初始化塊僅在初始化時才執行,且不過初始化幾次,它僅執行一次(這裡有一個條件,那就是只有它是被同一個類別載入器多次載入時,才是這樣,如果被不同的載入器,載入多次,則靜態初始化塊會執行多次)。
關於第三個參數請見下節介紹
3.2 直接使用類別載入器 java.lang.ClassLoader
在Java 之中,每個類別最後的老祖宗都是Object,而Object 裡有一個名為getClass()的方法,就是用來取得某特定實體所屬類別的參考,這個參考,指向的是一個名為Class 類別(Class.class) 的實體,您無法自行產生一個Class 類別的實體,因為它的建構式被宣告成private,這個Class 類別的實體是在類別檔(.class)第一次載入記憶體時就建立的,往後您在程式中產生任何該類別的實體,這些實體的內部都會有一個欄位記錄著這個Class 類別的所在位置。
基本上,我們可以把每個Class 類別的實體,當作是某個類別在記憶體中的代理人。每次我們需要
查詢該類別的資料(如其中的field、method 等)時,就可以請這個實體幫我們代勞。事實上,Java的Reflection 機制,就大量地利用Class 類別。去深入Class 類別的原始碼,我們可以發現Class類別的定義中大多數的方法都是原生方法(native method)。
在Java 之中,每個類別都是由某個類別載入器(ClassLoader 的實體)來載入,因此,Class 類別的實體中,都會有欄位記錄著載入它的ClassLoader 的實體(注意:如果該欄位是null,並不代表它不是由類別載入器所載入,而是代表這個類別由靴帶式載入器(bootstrap loader,也有人稱rootloader)所載入,只不過因為這個載入器並不是用Java 所寫成,是用C++寫的,所以邏輯上沒有實體)。
系統裡同時存在多個ClassLoader 的實體,而且一個類別載入器不限於只能載入一個類別,類別載入器可以載入多個類別。所以,只要取得Class 類別實體的參考,就可以利用其getClassLoader()方法籃取得載入該類別之類別載入器的參考。getClassLoader()方法最後會呼叫原生方法getClassLoader0(),其宣告如下:private native ClassLoader getClassLoader0();
最後,取得了ClassLoader 的實體,我們就可以叫用其loadClass()方法幫我們載入我們想要的類別,因此上面的Office類可做如下修改:
public class Office
{
public static void main(String[] args) throws Exception
{
Office off= new Office();
System.out.println("類別准備載入");
ClassLoader loader = off.getClass().getClassLoader();
java.lang.Class c = loader.loadClass(args[0]);
System.out.println("類別准備實體化");
Object o = c.newInstance();
Object o2 = c.newInstance();
}
}
其輸出結果同forName方法的第二個參數為false時相同。可見載入器載入類時只進行載入,不進行初始化。
獲取ClassLoader還可以用如下的方法:
public class Office
{
public static void main(String[] args) throws Exception
{
java.lang.Class cb = Office.class;
System.out.println("類別准備載入");
ClassLoader loader = cb.getClassLoader();
java.lang.Class c = loader.loadClass(args[0]);
System.out.println("類別准備實體化");
Object o = c.newInstance();
Object o2 = c.newInstance();
}
}
在此之前,當我們談到使用類別載入器來載入類別時,都是使用既有的類別載入器來幫我們載
入我們所指定的類別。那麽,我們可以自己產生類別載入器來幫我們載入類別嗎? 答案是肯定的。
利用Java 本身提供的java.net.URLClassLoader 類別就可以做到。
public class Office
{
public static void main(String[] args) throws Exception
{
URL u = new URL("file:/d:/myapp/classload/");
URLClassLoader ucl = new URLClassLoader(new URL[]{u});
java.lang.Class c = ucl.loadClass(args[0]);
Assembly asm = (Assembly)c.newInstance();
asm.start();
}
}
在這個范例中,我們自己產生java.net.URLClassLoader 的實體來幫我們載入我們所需要的類別。但是載入前,我們必須告訴URLClassLoader 去哪個地方尋找我們所指定的類別才行,所以我們必須給它一個URL 類別所構成的陣列,代表我們希望它去搜尋的所有位置。URL 可以指向網際網路上的任何位置,也可以指向我們電腦裡的檔案系統(包含JAR 檔)。在上述范例中,我們希望URLClassLoader 到d:mylib 這個目錄下去尋找我們需要的類別, 所以指定的URL為”file:/d:/my/lib/”。其實,如果我們請求的位置是主要類別(有public static void main(String args[])方法的那個類別)的相對目錄,我們可以在URL 的地方只寫”file:lib/”,代表相對於目前的目錄。
下面我們來看一下系統為我們提供的3個類別載入器:
java.exe 是利用幾個基本原則來尋找Java Runtime Environment(JRE),然後把類別檔(.class)直接轉交給JRE 執行之後,java.exe 就功成身退。類別載入器也是構成JRE 的其中一個重要成員,所以最後類別載入器就會自動從所在之JRE 目錄底下的lib t.jar 載入基礎類別函式庫。
當我們在命令列輸入java xxx.class 的時候,java.exe 根據我們之前所提過的邏輯找到了JRE(Java Runtime Environment),接著找到位在JRE 之中的jvm.dll(真正的Java 虛擬機器),最後載入這個動態聯結函式庫,啟動Java 虛擬機器。虛擬機器一啟動,會先做一些初始化的動作,比方說抓取系統參數等。一旦初始化動作完成之後,就會產生第一個類別載入器,即所謂的Bootstrap Loader,Bootstrap Loader 是由C++所撰寫而成(所以前面我們說,以Java 的觀點來看,邏輯上並不存在Bootstrap Loader 的類別實體,所以在Java 程式碼裡試圖印出其內容的時候,我們會看到的輸出為null),這個Bootstrap Loader 所
做的初始工作中,除了也做一些基本的初始化動作之外,最重要的就是載入定義在sun.misc 命名空間底下的Launcher.java 之中的ExtClassLoader(因為是inner class,所以編譯之後會變成Launcher$ExtClassLoader.class),並設定其Parent 為null,代表其父載入器為BootstrapLoader。然後Bootstrap Loader 再要求載入定義於sun.misc 命名空間底下的Launcher.java 之中的AppClassLoader(因為是inner class,所以編譯之後會變成Launcher$AppClassLoader.class),並設定其Parent 為之前產生的ExtClassLoader 實體。
這裡要請大家注意的是,Launcher$ExtClassLoader.class 與Launcher$AppClassLoader.class 都可能是由Bootstrap Loader 所載入,所以Parent 和由哪個類別載入器載入沒有關系。
三個載入器的層次關系可通過運行下面的例子察看:
public class Test
{
public static void main(String[] args)
{
ClassLoader cl1 = Test.class.getClassLoader();
System.out.println(cl1);
ClassLoader cl2 = cl1.getParent();
System.out.println(cl2);
ClassLoader cl3 = cl2.getParent();
System.out.println(cl3);
}
}
運行結果:
////////////////////////////////////////////////////////////
sun.misc.Launcher$AppClassLoader@1a0c10f
sun.misc.Launcher$ExtClassLoader@e2eec8
null
//////////////////////////////////////////////////////////
如果在上述程式中,如果您使用程式碼:
cl1.getClass.getClassLoader()及cl2.getClass.getClassLoader(),您會發現印出的都是null,
這代表它們都是由Bootstrap Loader 所載入。這裡也再次強調,類別載入器由誰載入(這句話有點
詭異,類別載入器也要由類別載入器載入,這是因為除了Bootstrap Loader 之外,其余的類別載
入器皆是由Java 撰寫而成),和它的Parent 是誰沒有關系,Parent 的存在只是為了某些特殊目的,
這個目的我們將在稍後作解釋。
在此要請大家注意的是,AppClassLoader 和ExtClassLoader 都是URLClassLoader 的子類別。
由於它們都是URLClassLoader 的子類別,所以它們也應該有URL 作為搜尋類別檔的參考,由原始碼
中我們可以得知,AppClassLoader 所參考的URL 是從系統參數java.class.path 取出的字串所決定,
而java.class.path 則是由我們在執行java.exe 時,利用–cp 或-classpath 或CLASSPATH 環境變
數所決定。
用如下示例測試:
public class AppLoader
{
public static void main(String[] args)
{
String s = System.getProperty("java.class.path");
System.out.println(s);
}
}
/////////////////////////////////////////////////////////////////
D:myappclassload>java AppLoader
.;D:myjavaTomcat5.0webappsaxisWEB-INFlibaxis.jar;D:myjavaTomcat5.0weba
ppsaxisWEB-INFlibcommons-logging.jar;D:myjavaTomcat5.0webappsaxisWEB-IN
Flibcommons-discovery.jar;C:oracleora81jdbclibclasses12.zip;D:myjavaJDB
CforSQLserverlibmssqlserver.jar;D:myjavaJDBCforSQLserverlibmsbase.jar;D:m
yjavaJDBCforSQLserverlibmsutil.jar;D:myjavaTomcat5.0commonlibservlet-api
.jar;D:myjavaj2sdk1.4.2_04jrelib t.jar;C:sunappserverlibj2ee.jar;D:myj
avaj2sdk1.4.2_04libjaxp.jar;D:myjavaj2sdk1.4.2_04libsax.jar;
D:myappclassload>java -classpath .;d:myapp AppLoader
.;d:myapp
/////////////////////////////////////////////////////////////////
從這個輸出結果,我們可以看出,在預設情況下,AppClassLoader 的搜尋路徑為”.”(目前所在目
錄),如果使用-classpath 選項(與-cp 等效),就可以改變AppClassLoader 的搜尋路徑,如果沒有
指定-classpath 選項,就會搜尋環境變數CLASSPATH。如果同時有CLASSPATH 的環境設定與
-classpath 選項,則以-classpath 選項的內容為主,CLASSPATH 的環境設定與-classpath 選項兩者
的內容不會有加成的效果。
至於ExtClassLoader 也有相同的情形,不過其搜尋路徑是參考系統參數java.ext.dirs。
系統參數java.ext.dirs 的內容,會指向java.exe 所選擇的JRE 所在位置下的libext 子目錄。Java.exe使用的JRE是在系統變量path裡指定的,可以通過修改path從而修改ExtCLassLoader的搜尋路徑,也可以如下命令參數來更改,
java –Djava.ext.dirs=c:winnt AppLoader //注意 =號兩邊不能有空格。-D也不能和java分開。
////////////////////////////////////////////////////////////////
D:myappclassload>java ExtLoader
D:myjavaj2sdk1.4.2_04jrelibext
D:myappclassload>java -Djava.ext.dirs=c:winnt ExtLoader
c:winnt
////////////////////////////////////////////////////////////////
最後一個類別載入器是Bootstrap Loader , 我們可以經由查詢由系統參數sun.boot.class.path 得知Bootstrap Loader 用來搜尋類別的路徑。該路徑的修改與ExtClassLoader的相同。但修改後不影響Bootstrap的搜尋路徑。
在命令列下參數時,使用–classpath / -cp / 環境變數CLASSPATH 來更改AppClassLoader的搜尋路徑,或者用–Djava.ext.dirs 來改變ExtClassLoader 的搜尋目錄,兩者都是有意義的。
可是用–Dsun.boot.class.path 來改變Bootstrap Loader 的搜尋路徑是無效。這是因為
AppClassLoader 與ExtClassLoader 都是各自參考這兩個系統參數的內容而建立,當您在命令列下
變更這兩個系統參數之後, AppClassLoader 與ExtClassLoader 在建立實體的時候會參考這兩個系
統參數,因而改變了它們搜尋類別檔的路徑;而系統參數sun.boot.class.path 則是預設與
Bootstrap Loader 的搜尋路徑相同,就算您更改該系統參與,與Bootstrap Loader 完全無關。
改變java.exe所使用的jre會改變Bootstrap Loader的搜尋路徑。
Bootstrap Loader的搜尋路徑一般如下:
///////////////////////////////////////////////////////////////////////////////////
D:myjavaj2sdk1.4.2_04jrelib t.jar;D:myjavaj2sdk1.4.2_04jrelibi18n.jar;
D:myjavaj2sdk1.4.2_04jrelibsunrsasign.jar;D:myjavaj2sdk1.4.2_04jrelibj
sse.jar;D:myjavaj2sdk1.4.2_04jrelibjce.jar;D:myjavaj2sdk1.4.2_04jrelib
charsets.jar;D:myjavaj2sdk1.4.2_04jreclasses
///////////////////////////////////////////////////////////////////////////////////////
更重要的是,AppClassLoader 與ExtClassLoader 在整個虛擬機器之中只會存有一份,一旦建
立了,其內部所參考的搜尋路徑將不再改變,也就是說,即使我們在程式裡利用System.setProperty()
來改變系統參數的內容,仍然無法更動AppClassLoader 與ExtClassLoader 的搜尋路徑。因此,執
行時期動態更改搜尋路徑的設定是不可能的事情。如果因為特殊需求,有些類別的所在路徑並非在
一開始時就能決定,那麽除了產生新的類別載入器來輔助我們載入所需的類別之外,沒有其他方法了。
下面我們將看一下載入器的委派模型
所謂的委派模型,用簡單的話來講,就是「類別載入器有載入類別的需求時,會先請示其Parent 使用其搜尋路徑幫忙載入,如果Parent 找不到,那麽才由自己依照自己的搜尋路徑搜尋類別」。
下面我們看一下小的示例:
public class Test
{
public static void main(String[] args)
{
System.out.println(Test.class.getClassLoader());
TestLib tl = new TestLib();
tl.start();
}
}
public class TestLib
{
public void start()
{
System.out.println(this.getClass().getClassLoader());
}
}
如果這兩個類僅放在dos命令提示符的當前目錄下,則輸出結果如下:
//////////////////////////////////////////////////////
sun.misc.Launcher$AppClassLoader@1a0c10f
sun.misc.Launcher$AppClassLoader@1a0c10f
//////////////////////////////////////////////////////
如果這兩個類同時又放在<JRE 所在目錄>libextclasses 底下(在我的機器上是:D:myjavaj2sdk1.4.2_04jrelibextclasses,classes沒有,需要自己建),輸出結果如下:
/////////////////////////////////
sun.misc.Launcher$ExtClassLoader@e2eec8
sun.misc.Launcher$ExtClassLoader@e2eec8
////////////////////////////////////
最後如果在<JRE 所在目錄>classes下放入這兩個類,則輸出結果為
/////////////////////////////////
null
null
////////////////////////////////////
如果把<JRE 所在目錄>classes下的TestLib刪去,則輸出入下:
//////////////////////////////////////
null
Exception in thread "main" java.lang.NoClassDefFoundError: TestLib
at Test.main(Test.java:7)
//////////////////////////////////////
這是因為Test的classLoader是Bootstrap Loader ,因此TestLib的也默認為是Bootstrap Loader。Bootstrap Loader搜尋路徑下的TestLib被刪去了,Bootstrap Loader又沒有parent,所以提示找不到。
其他的情況可以自己逐個添加或刪除文件,然後執行java Test進行測試,察看輸出結果。
AppClassLoader 與Bootstrap Loader會搜尋它們所指定的位置(或JAR 檔),如果找不到就找不到了,AppClassLoader 與Bootstrap Loader不會遞回式地搜尋這些位置下的其他路徑或其他沒有被指定的JAR 檔。反觀ExtClassLoader,所參考的系統參數是java.ext.dirs,意思是說,他會搜尋底下的所有JAR 檔以及classes 目錄,作為其搜尋路徑。
*