2007年9月25日 星期二

Java 學習筆記 (10) - Reflection

class 的載入與檢視

class 載入

為了妥善使用有限的資源,Java 在真正需要使用到 class 的時候才會將其載入,當每一個 class 被載入時,JVM 就會為其自動產生一個 Class object。

【註】每一個 class 載入僅會有一個 Class object。因此即使使用同一個 class 產生多次 object,也只會有一個與其對應 Class object。

每一個 object 都可以透過 getClass() 的方式取得 Class object,以下為一個簡單的範例:
public class ClassDemo {
public static void main(String args[]) {
String name = "godleon";
Class stringClass = name.getClass();

System.out.println("類別名稱:" + stringClass.getName());
System.out.println("是否為介面:" + stringClass.isInterface());
System.out.println("是否為基本型態:" + stringClass.isPrimitive());
System.out.println("是否為陣列物件:" + stringClass.isArray());
System.out.println("父類別名稱:" + stringClass.getSuperclass().getName());
}
}

而 Java 只有在真正要用到 class 的時候才會將其載入,而真正用到的時候是指以下情況:
  1. 使用 class 生成 object 時
  2. 使用者指定要載入 class (利用 Class.forName() 或是 ClassLoader.loadClass())

若僅是宣告並不會載入 class,以下用一段程式碼來測試:

TestClass.java
public class TestClass {
static {
System.out.println("類別被載入");
}
}
LoadClassTest.java
public class LoadClassTest {
public static void main(String args[]) {
TestClass test = null; //class不會載入,因此不會顯示「類別被載入」
System.out.println("宣告 TestClass 參考名稱");
test = new TestClass(); //class被載入,顯示「類別被載入」
System.out.println("生成 TestClass 實例");
}
}

此外,由於 Java 支援 Run-Time Type Identification(RTTI,執行時期型別辨識),因此 Class 的訊息在編譯時期就會被加入 .class 檔案中;而執行時,JVM 則會在使用某 class 時,會先檢查相對應的 Class object 是否已經載入,如果沒有載入,則會尋找相對應的 .class 檔案載入。

而一個 class 在 JVM 中只會有一個 Class object,所以每個 object 都可以透過 getClass() 或是 .class 屬性取得 Class object。

之前有說過,在 Java 中任何東西皆為 object,因此 Array 也不例外,甚至如 primitive type、關鍵字 void 都有相對應的 Class object,以下用一段程式來說明:
public class ClassDemo2 {
public static void main(String args[]) {
System.out.println(boolean.class); //boolean
System.out.println(void.class); //void

int[] intAry = new int[10];
System.out.println(intAry.getClass().toString()); //class [I

double[] dblAry = new double[10];
System.out.println(dblAry.getClass().toString()); //class [D
}
}
Class 類別中的 forNamenewInstance 方法

若要動態載入類別,可以使用 Class 類別中的 forName() method,此為 static method,因此可以直接透過 Class 類別使用;另外,若要取得類別的 instance,可使用 Class 類別中的 newInstance() method,以下用依段程式來說明兩個 method 的使用方法:
import java.util.*;

public class ClassForNameDemo {
public static void main(String[] args) {
try {
//forName的使用
Class c = Class.forName(args[0]);
System.out.println("getName : " + c.getName());
System.out.println("isInterface : " + c.isInterface());
System.out.println("isPrimitive : " + c.isPrimitive());
System.out.println("isArray : " + c.isArray());
System.out.println("SuperClass : " + c.getCanonicalName());

//newInstance的使用
List lst = (List) c.newInstance();
for(int i = 0 ; i < 10 ; i++)
lst.add("element" + i);
for(Object o : lst.toArray())
System.out.println(o);
}
catch(Exception e) {
e.printStackTrace();
}
}
}
而當使用 newInstance() 取得 instance 後,就可以使用 getClassLoader()getConstructor()getFields()getMethods()getModifiers()getPackage() ..... 等方法,取得 Class 所產生的 instance 的相關資訊,而類別檢視器就是利用這樣子的方式所做出來的!


Class Loader

當 Java 需要使用到 Class 時,才會將 Class 載入;而載入 Class 的工作是由 Class Loader(類別載入器) 所負責。

每當 Java 程式啟動後,會有以下三種 Class Loader 進行載入 Class 的工作:
  1. Bootstrap Loader
    由 C++ 開發,會去搜尋 JRE 目錄($JAVA_HOME/jre/)中的 class 以及 lib 資料夾中的 *.jar 檔,檢查是否有指定的 class 需要載入。
  2. ExtClassLoader
    會去搜尋 JRE 目錄($JAVA_HOME/jre/)中的 lib/ext 資料夾,檢查其中的 class 與 *.jat 檔案,是否有指定的 class 需要載入。
  3. AppClassLoader
    搜尋 classpath 中是否有指定的 class 需要載入。
以下用一張圖形來說明三個 Class Loader 的階層關係:

其中 Bootstrap Loader 為 ExtClassLoader 的 Parent Class;而 ExtClassLoader 則是 AppClassLoader 的 Parent Class。

而從 JVM 啟動後,載入的順序為 Bootstrap Loader -> ExtClassLoader -> AppClassLoader。

而每個 class 所產生出來的 instance 都可以使用 getClassLoader() 知道負責載入 class 的是哪一個 class loader。


動態載入類別、產生物件、呼叫方法

接著在這個部分,要介紹的是如何動態的載入將已經寫好的 class,並產生物件來呼叫 class 中所包含的 method,以下用兩段程式說明:

Student.java
public class Student {
private String name;
private int score;

public Student() {
name = "N/A";
}
public Student(String name, int score) {
this.name = name;
this.score = score;
}

public void setName(String name) {
this.name = name;
}
public void setScore(int score) {
this.score = score;
}

public String getName() {
return name;
}
public int getScore() {
return score;
}

public void showData() {
System.out.println("Name : " + this.name);
System.out.println("Score : " + this.score);
}
}
newInstanceDemo.java
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class newInstanceDemo {
public static void main(String[] args) {
Class c = null;
try {
c = Class.forName(args[0]);

/* (Begin)=============== 生成物件 ===============(Begin) */

//指定Constructor所使用的參數型態
Class[] oParam = new Class[2];
oParam[0] = String.class;
oParam[1] = Integer.TYPE;

//產生Constructor
Constructor constructor = c.getConstructor(oParam);

//指定參數的內容
Object[] paramObjs = new Object[2];
paramObjs[0] = "godleon";
paramObjs[1] = new Integer(90);

//透過Constructor產生物件
Object obj = constructor.newInstance(paramObjs);
System.out.println(obj);

/* (End)=============== 生成物件 ===============(End) */


/* (Begin)=============== 呼叫方法 ===============(Begin) */

//指定Method所使用的參數類型
Class[] mParam1 = {String.class}; //只有一個參數

//產生Method(指定method名稱與參數)
Method setName = c.getMethod("setName", mParam1);

//指定參數內容
Object[] mParamObjs1 = {"godleon"};

//呼叫方法
setName.invoke(obj, mParamObjs1); //呼叫setName方法

//指定Method所使用的參數類型
Class[] mParam2 = {Integer.TYPE};

//產生Method(指定method名稱與參數)
Method setScore = c.getMethod("setScore", mParam2);

//指定參數內容
Object[] mParamObjs2 = {new Integer(90)};

//呼叫方法
setScore.invoke(obj, mParamObjs2); //呼叫setScore方法

//產生Method(指定method名稱與參數)
Method showData = c.getMethod("showData", null);

//呼叫方法
showData.invoke(obj, null); //呼叫showData方法

/* (End)=============== 呼叫方法 ===============(End) */
}
catch(Exception e) {
e.printStackTrace();
}
}
}
由上面的程式碼可以知道,若是有執行時期動態載入 class,並呼叫其 method 的需求,透過使用 reflection,可以很簡單達成目的。

另外也可以動態的修改 Field 的值,可透過 getField() method 來達成,使用方式跟上面的程式大同小異,而若是要瞭解詳細的使用方式,可以參考此篇文章

最後,此篇筆記就是著重在瞭解 Java 如何在執行期間動態載入 class 並使用,未來有機會用到的時候再來繼續深入探討.....

7 則留言:

  1. It's reallly good nice article about the Java reflection...
    pinos

    回覆刪除
  2. 真的寫的很棒,又學到一些知識,感謝你的教學文章

    回覆刪除
  3. 謝謝你的文章 很有用的解釋

    回覆刪除
  4. 寫得很詳盡~感謝分享

    回覆刪除
  5. 執行應該加java.util.Stack

    回覆刪除
  6. 謝謝您的文章
    站在巨人的肩膀上 讓我看得更遠 有更好的發展

    回覆刪除