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:/my/lib/ 這個目錄下去尋找我們需要的類別, 所以指定的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/rt.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:/myapp/classload>java AppLoader
.;D:/myjava/Tomcat5.0/webapps/axis/WEB-INF/lib/axis.jar;D:/myjava/Tomcat5.0/weba
pps/axis/WEB-INF/lib/commons-logging.jar;D:/myjava/Tomcat5.0/webapps/axis/WEB-IN
F/lib/commons-discovery.jar;C:/oracle/ora81/jdbc/lib/classes12.zip;D:/myjava/JDB
CforSQLserver/lib/mssqlserver.jar;D:/myjava/JDBCforSQLserver/lib/msbase.jar;D:/m
yjava/JDBCforSQLserver/lib/msutil.jar;D:/myjava/Tomcat5.0/common/lib/servlet-api
.jar;D:/myjava/j2sdk1.4.2_04/jre/lib/rt.jar;C:/sun/appserver/lib/j2ee.jar;D:/myj
ava/j2sdk1.4.2_04/lib/jaxp.jar;D:/myjava/j2sdk1.4.2_04/lib/sax.jar;
D:/myapp/classload>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 所在位置下的/lib/ext 子目錄。Java.exe使用的JRE是在系统变量path里指定的,可以通过修改path从而修改ExtCLassLoader的搜寻路径,也可以如下命令参数來更改,
java –Djava.ext.dirs=c:/winnt/ AppLoader //注意 =号两边不能有空格。-D也不能和java分开。
D:/myapp/classload>java ExtLoader
D:/myjava/j2sdk1.4.2_04/jre/lib/ext
D:/myapp/classload>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:/myjava/j2sdk1.4.2_04/jre/lib/rt.jar;D:/myjava/j2sdk1.4.2_04/jre/lib/i18n.jar;
D:/myjava/j2sdk1.4.2_04/jre/lib/sunrsasign.jar;D:/myjava/j2sdk1.4.2_04/jre/lib/j
sse.jar;D:/myjava/j2sdk1.4.2_04/jre/lib/jce.jar;D:/myjava/j2sdk1.4.2_04/jre/lib/
charsets.jar;D:/myjava/j2sdk1.4.2_04/jre/classes
///
更重要的是,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 所在目錄>/lib/ext/classes 底下(在我的机器上是:D:/myjava/j2sdk1.4.2_04/jre/lib/ext/classes,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 目錄,作為其搜尋路徑。