Reflection是Java 程式開發語言的特徵之一,它允許運行中的 Java 程式對自身進行檢查,或者說"自審",並能直接操作程式的內部屬性。例如,使用它能獲得 Java 類中各成員的名稱並顯示出來。
Java 的這一能力在實際應用中也許用得不是很多,但是在其它的程式設計語言中根本就不存在這一特性。例如,Pascal、C 或者 C++ 中就沒有辦法在程式中獲得函數定義相關的資訊。
JavaBean 是 reflection 的實際應用之一,它能讓一些工具可視化的操作軟體組件。這些工具通過 reflection 動態載入並取得 Java 組件(類) 的屬性。
1. 一個簡單的例子
考慮下面這個簡單的例子,讓我們看看 reflection 是如何工作的。
import java.lang.reflect.*; public class DumpMethods { public static void main(String args[]) { try { Class c = Class.forName(args[0]); Method m[] = c.getDeclaredMethods(); for (int i = 0; i < m.length; i++) System.out.println(m[i].toString()); } catch (Throwable e) { System.err.println(e); } } }
|
按如下語句執行:
java DumpMethods java.util.Stack
它的結果輸出為:
public java.lang.Object java.util.Stack.push(java.lang.Object)
public synchronized java.lang.Object java.util.Stack.pop()
public synchronized java.lang.Object java.util.Stack.peek()
public boolean java.util.Stack.empty()
public synchronized int java.util.Stack.search(java.lang.Object) |
這樣就列出了java.util.Stack 類的各方法名以及它們的限制符和傳回型別。
這個程式使用 Class.forName 載入指定的類,然後調用 getDeclaredMethods 來擷取這個類中定義了的方法列表。java.lang.reflect.Methods 是用來描述某個類中單個方法的一個類。
2.開始使用 Reflection
用於 reflection 的類,如 Method,可以在 java.lang.relfect 包中找到。使用這些類的時候必須要遵循三個步驟:第一步是獲得你想操作的類的 java.lang.Class 對象。在運行中的 Java 程式中,用 java.lang.Class 類來描述類和介面等。
下面就是獲得一個 Class 對象的方法之一:
Class c = Class.forName("java.lang.String");
這條語句得到一個 String 類的類對象。還有另一種方法,如下面的語句:
Class c = int.class;
或者
Class c = Integer.TYPE;
它們可獲得基本類型的類資訊。其中後一種方法中訪問的是基本類型的封裝類 (如 Integer) 中預先定義好的 TYPE 欄位。
第二步是調用諸如 getDeclaredMethods 的方法,以取得該類中定義的所有方法的列表。
一旦取得這個資訊,就可以進行第三步了——使用 reflection API 來操作這些資訊,如下面這段代碼:
Class c = Class.forName("java.lang.String");
Method m[] = c.getDeclaredMethods();
System.out.println(m[0].toString());
它將以文本方式列印出 String 中定義的第一個方法的原型。
在下面的例子中,這三個步驟將為使用 reflection 處理特殊應用程式提供例證。
類比 instanceof 操作符
得到類資訊之後,通常下一個步驟就是解決關於 Class 對象的一些基本的問題。例如,Class.isInstance 方法可以用於類比 instanceof 操作符:
class A { }
public class instance1 { public static void main(String args[]) { try { Class cls = Class.forName("A"); boolean b1 = cls.isInstance(new Integer(37)); System.out.println(b1); boolean b2 = cls.isInstance(new A()); System.out.println(b2); } catch (Throwable e) { System.err.println(e); } } } |
在這個例子中建立了一個 A 類的 Class 對象,然後檢查一些對象是否是 A 的執行個體。Integer(37) 不是,但 new A()是。
3.找出類的方法
找出一個類中定義了些什麼方法,這是一個非常有價值也非常基礎的 reflection 用法。下面的代碼就實現了這一用法:
import java.lang.reflect.*;
public class method1 { private int f1(Object p, int x) throws NullPointerException { if (p == null) throw new NullPointerException(); return x; }
public static void main(String args[]) { try { Class cls = Class.forName("method1"); Method methlist[] = cls.getDeclaredMethods(); for (int i = 0; i < methlist.length; i++) { Method m = methlist[i]; System.out.println("name = " + m.getName()); System.out.println("decl class = " + m.getDeclaringClass()); Class pvec[] = m.getParameterTypes(); for (int j = 0; j < pvec.length; j++) System.out.println("param #" + j + " " + pvec[j]); Class evec[] = m.getExceptionTypes(); for (int j = 0; j < evec.length; j++) System.out.println("exc #" + j + " " + evec[j]); System.out.println("return type = " + m.getReturnType()); System.out.println("-----"); } } catch (Throwable e) { System.err.println(e); } } }
|
這個程式首先取得 method1 類的描述,然後調用 getDeclaredMethods 來擷取一系列的 Method 對象,它們分別描述了定義在類中的每一個方法,包括 public 方法、protected 方法、package 方法和 private 方法等。如果你在程式中使用 getMethods 來代替 getDeclaredMethods,你還能獲得繼承來的各個方法的資訊。
取得了 Method 對象列表之後,要顯示這些方法的參數類型、異常類型和傳回值類型等就不難了。這些類型是基本類型還是類類型,都可以由描述類的對象按順序給出。
輸出的結果如下:
name = f1
decl class = class method1
param #0 class java.lang.Object
param #1 int
exc #0 class java.lang.NullPointerException
return type = int
----- name = main
decl class = class method1
param #0 class [Ljava.lang.String;
return type = void
|
4.擷取構造器資訊
擷取類構造器的用法與上述擷取方法的用法類似,如:
import java.lang.reflect.*;
public class constructor1 { public constructor1() { }
protected constructor1(int i, double d) { }
public static void main(String args[]) { try { Class cls = Class.forName("constructor1"); Constructor ctorlist[] = cls.getDeclaredConstructors(); for (int i = 0; i < ctorlist.length; i++) { Constructor ct = ctorlist[i]; System.out.println("name = " + ct.getName()); System.out.println("decl class = " + ct.getDeclaringClass()); Class pvec[] = ct.getParameterTypes(); for (int j = 0; j < pvec.length; j++) System.out.println("param #" + j + " " + pvec[j]); Class evec[] = ct.getExceptionTypes(); for (int j = 0; j < evec.length; j++) System.out.println("exc #" + j + " " + evec[j]); System.out.println("-----"); } } catch (Throwable e) { System.err.println(e); } } } |
這個例子中沒能獲得傳回型別的相關資訊,那是因為構造器沒有傳回型別。
這個程式啟動並執行結果是:
name = constructor1
decl class = class constructor1
----- name = constructor1
decl class = class constructor1
param #0 int
param #1 double
|
5.擷取類的欄位(域)
找出一個類中定義了哪些資料欄位也是可能的,下面的代碼就在幹這個事情:
import java.lang.reflect.*;
public class field1 { private double d; public static final int i = 37; String s = "testing";
public static void main(String args[]) { try { Class cls = Class.forName("field1"); Field fieldlist[] = cls.getDeclaredFields(); for (int i = 0; i < fieldlist.length; i++) { Field fld = fieldlist[i]; System.out.println("name = " + fld.getName()); System.out.println("decl class = " + fld.getDeclaringClass()); System.out.println("type = " + fld.getType()); int mod = fld.getModifiers(); System.out.println("modifiers = " + Modifier.toString(mod)); System.out.println("-----"); } } catch (Throwable e) { System.err.println(e); } } }
|
這個例子和前面那個例子非常相似。例中使用了一個新東西 Modifier,它也是一個 reflection 類,用來描述欄位成員的修飾語,如“private int”。這些修飾語自身由整數描述,而且使用 Modifier.toString 來返回以“官方”順序排列的字串描述 (如“static”在“final”之前)。這個程式的輸出是:
name = d
decl class = class field1
type = double
modifiers = private
----- name = i
decl class = class field1
type = int
modifiers = public static final
----- name = s
decl class = class field1
type = class java.lang.String
modifiers =
|
和擷取方法的情況一下,擷取欄位的時候也可以只取得在當前類中申明了的欄位資訊 (getDeclaredFields),或者也可以取得父類中定義的欄位 (getFields) .
6.根據方法的名稱來執行方法
文本到這裡,所舉的例子無一例外都與如何擷取類的資訊有關。我們也可以用 reflection 來做一些其它的事情,比如執行一個指定了名稱的方法。下面的樣本示範了這一操作:
import java.lang.reflect.*; public class method2 { public int add(int a, int b) { return a + b; } public static void main(String args[]) { try { Class cls = Class.forName("method2"); Class partypes[] = new Class[2]; partypes[0] = Integer.TYPE; partypes[1] = Integer.TYPE; Method meth = cls.getMethod("add", partypes); method2 methobj = new method2(); Object arglist[] = new Object[2]; arglist[0] = new Integer(37); arglist[1] = new Integer(47); Object retobj = meth.invoke(methobj, arglist); Integer retval = (Integer) retobj; System.out.println(retval.intValue()); } catch (Throwable e) { System.err.println(e); } } }
|
假如一個程式在執行的某處的時候才知道需要執行某個方法,這個方法的名稱是在程式的運行過程中指定的 (例如,JavaBean 開發環境中就會做這樣的事),那麼上面的程式示範了如何做到。
上例中,getMethod用於尋找一個具有兩個整型參數且名為 add 的方法。找到該方法並建立了相應的Method 對象之後,在正確的對象執行個體中執行它。執行該方法的時候,需要提供一個參數列表,這在上例中是分別封裝了整數 37 和 47 的兩個 Integer 對象。執行方法的返回的同樣是一個 Integer 對象,它封裝了傳回值 84.
7.建立新的對象
對於構造器,則不能像執行方法那樣進行,因為執行一個構造器就意味著建立了一個新的對象 (準確的說,建立一個對象的過程包括分配記憶體和構造對象)。所以,與上例最相似的例子如下:
import java.lang.reflect.*;
public class constructor2 { public constructor2() { }
public constructor2(int a, int b) { System.out.println("a = " + a + " b = " + b); }
public static void main(String args[]) { try { Class cls = Class.forName("constructor2"); Class partypes[] = new Class[2]; partypes[0] = Integer.TYPE; partypes[1] = Integer.TYPE; Constructor ct = cls.getConstructor(partypes); Object arglist[] = new Object[2]; arglist[0] = new Integer(37); arglist[1] = new Integer(47); Object retobj = ct.newInstance(arglist); } catch (Throwable e) { System.err.println(e); } } } |
根據指定的參數類型找到相應的建構函式並執行它,以建立一個新的對象執行個體。使用這種方法可以在程式運行時動態地建立對象,而不是在編譯的時候建立對象,這一點非常有價值。
8.改變欄位(域)的值
reflection 的還有一個用處就是改變對象資料欄位的值。reflection 可以從正在啟動並執行程式中根據名稱找到對象的欄位並改變它,下面的例子可以說明這一點:
import java.lang.reflect.*;
public class field2 { public double d;
public static void main(String args[]) { try { Class cls = Class.forName("field2"); Field fld = cls.getField("d"); field2 f2obj = new field2(); System.out.println("d = " + f2obj.d); fld.setDouble(f2obj, 12.34); System.out.println("d = " + f2obj.d); } catch (Throwable e) { System.err.println(e); } } } |
這個例子中,欄位 d 的值被變為了 12.34.
9.使用數組
本文介紹的 reflection 的最後一種用法是建立的運算元組。數組在 Java 語言中是一種特殊的類類型,一個數組的引用可以賦給 Object 引用。觀察下面的例子看看數組是怎麼工作的:
import java.lang.reflect.*;
public class array1 { public static void main(String args[]) { try { Class cls = Class.forName("java.lang.String"); Object arr = Array.newInstance(cls, 10); Array.set(arr, 5, "this is a test"); String s = (String) Array.get(arr, 5); System.out.println(s); } catch (Throwable e) { System.err.println(e); } } } |
例中建立了 10 個單位長度的 String 數組,為第 5 個位置的字串賦了值,最後將這個字串從數組中取得並列印了出來。
下面這段代碼提供了一個更複雜的例子:
import java.lang.reflect.*;
public class array2 { public static void main(String args[]) { int dims[] = new int[]{5, 10, 15}; Object arr = Array.newInstance(Integer.TYPE, dims); Object arrobj = Array.get(arr, 3); Class cls = arrobj.getClass().getComponentType(); System.out.println(cls); arrobj = Array.get(arrobj, 5); Array.setInt(arrobj, 10, 37); int arrcast[][][] = (int[][][]) arr; System.out.println(arrcast[3][5][10]); } } |
例中建立了一個 5 x 10 x 15 的整型數組,並為處於 [3][5][10] 的元素賦了值為 37.注意,多維陣列實際上就是數組的數組,例如,第一個 Array.get 之後,arrobj 是一個 10 x 15 的數組。進而取得其中的一個元素,即長度為 15 的數組,並使用 Array.setInt 為它的第 10 個元素賦值。
注意建立數組時的類型是動態,在編譯時間並不知道其類型。