參考自–《Java核心技術卷1》
Java 的反射庫提供了一個非常豐富且精心設計的工具集,以便編寫能夠動態操縱 Java 代碼的程序。這項功能被大量地應用於 JavaBeans 中,它是 Java 組件的體系結構。
能夠分析類能力的程序稱爲反射。反射機制的功能極其強大,它可以用來:
- 在運行時分析類
- 在運行時查看對象
- 實現通用的數組操作代碼
- 利用 Method 對象
反射是一種功能強大且複雜的機制。
1 Class 類
在程序運行期間,Java 運行時系統始終爲所有的對象維護一個被稱爲運行時的類型標識。這個標識信息跟蹤着每個對象所屬的類。虛擬機利用運行時類型信息找到相應的方法並執行下去。
可以通過專門的 Java 類訪問這些信息(運行時類型),保存這些信息的類被稱爲 Class。
獲取 Class 類對象的方法:
1)Object 類中的 getClass()
方法將會返回一個 Class 類型的實例。
Employee e = new Employee(); //創建一個僱員對象
Manager m = new Manager(); //創建一個經理對象
Class c1 = e.getClass();
Class c2 = m.getClass();
如同用一個 Employee 對象表示一個特定的僱員屬性一樣,一個 Class 對象將表示一個特定類的屬性。最常用的 Class 方法是 getName
。這個方法將返回類的名字,例如:
System.out.println(e.getClass().getName()); //輸出 class cn.cbq.study.sample01.Employee
System.out.println(m.getClass().getName()); //輸出 class cn.cbq.study.sample01.Manager
如果類在一個包裏,包的名字也作爲類名的一部分輸出。
2)還可以調用靜態方法 forName
獲取類名對應的 Class 對象:
String className = "java.util.Random";
try {
Class c = Class.forName(className);
System.out.println(c.getName()); //輸出 java.util.Random
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
如果類名保存在字符串中,並可在運行中改變,就可以使用這個方法。當然,這個方法只有在字符串爲類名或接口名時才能夠執行,否則,forName
方法將拋出異常(無論何時使用這個方法,都應該提供一個異常處理器)。
注:在啓動 Java 程序時,包含 main 方法的類被加載,它又會加載所有它需要的類。這些被加載的類又要加載它們各自需要的類,以次類推。對於一個大型的應用程序來說,這將消耗很多時間。可以使用這樣一個技巧,給人一種啓動速度比較快的幻覺:在 main 方法中通過調用 Class.forName
手工地加載其他的類,但需確保包含 main 方法的類沒有顯式地引用其他的類。
3)如果 T 是任意的 Java 類型(或 void 關鍵字),T.class 將代表匹配的類對象:
Class c1 = Random.class; //要先 import java.util.*;否則 c1 = java.util.Random.class;
Class c2 = int.class; // 輸出 int
Class c3 = Double[].class; //輸出 class [Ljava.lang.Double;
注意,一個 Class 對象實際上表示的是一個類型,而這個類型未必一定是一種類。例如,int 不是類,但 int.class 是一個 Class 類型的對象。
注:Class 類實際上是一個泛型類。例如,Employee.class 的類型是 Class<Employee>
。它將抽象的概念更加複雜化了,在大多數實際問題中,可以忽略類型參數,而使用原始的 Class 類。
getName 方法在應用於數組類型的時候會返回比較奇怪的名字:
- Double[].class.getName() 返回 “[Ljava.lang.Double”
- int[].class.getName() 返回 “[I”
虛擬機爲每個類型管理一個 Class 對象。可以使用 == 運算符實現兩個類對象比較的操作:
if(e.getClass()==Employee.class) ...
還有一個很有用的方法 newInstance()
,可以用來動態地創建一個類的實例:
Employee e = new Employee();
try {
Employee es = e.getClass().newInstance(); //創建了一個新的Employee實例 es
} catch (InstantiationException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
}
創建了一個與 e 具有相同類類型的實例。 newInstance()
方法調用默認的構造器(沒有參數的構造器)初始化新創建的對象。若這個類沒有默認的構造器(有其他帶參數的構造器),就會拋出一個異常。
將 forName
與 newInstance()
配合起來使用,就可以根據存儲在字符串中的類名創建一個對象:
String className = "java.util.Random";
try {
Object obj = Class.forName(s).newInstance(); //創建一個Random類對象
Random ran = (Random)obj;
int i = ran.nextInt(); //使用 Random 對象生成一個隨機 int 類型數值
} catch (InstantiationException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
注:如果需要以這種方式向希望按名稱創建的類的構造器提供參數(調用帶參數的構造函數),就需要使用 Constructor 類中的 newInstance()
方法。
2 利用反射分析類的能力(檢查類的結構)
在 java.lang.reflect 包中有三個類 Field,Method 和 Constructor 分別用於描述類的域、方法和構造器。
這三個類都有一個叫 getName
的方法,用來返回項目的名稱。Field 類有一個 getType
方法,用來返回描述域所屬類型的 Class 對象。Method 和 Constructor 類有能夠報告參數類型的方法,Method 類還有一個可以報告返回類型的方法。這三個類還有一個叫 getModifies
的方法,它將返回一個整型數值,用不同的位開關描述 public 和 static 這樣的修飾符使用狀況。另外,還可以利用 java.lang.reflect 包中的 Modifier 類的靜態方法分析 getModifies
返回的整型數值。例如,可以使用 Modifier 類中的 isPublic
、isPrivate
或 isFinal
判斷方法或構造器是否是 public、private 或 final。另外還可以利用 Modifier.toString
方法將修飾符打印出來。
Class 類中的 getFields
、getMethods
和 getConstructors
方法將分別返回類提供的 public 域、方法和構造器數組,其中包括超類的公有成員。Class 類的 getDeclaredFields
、getDeclaredMethods
和 getDeclaredConstructors
方法將分別返回類中聲明的全部域、方法和構造器,其中包括私有和受保護成員,但不包括超類的成員。
下面的案例通過輸入 Java 類 分析 域,方法和構造器:
import java.util.*;
import java.lang.reflect.*;
public class ReflectionTest {
public static void main(String[] args) {
// read class name from command line args or user input
String name;
if (args.length > 0) {
name = args[0];
}else {
Scanner in = new Scanner(System.in);
System.out.println("Enter class name (例如:java.util.Date): ");
name = in.next();
}
try {
// print class name and superclass name (if != Object)
Class cl = Class.forName(name);
Class supercl = cl.getSuperclass();
String modifiers = Modifier.toString(cl.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print("class " + name);
if (supercl != null && supercl != Object.class)
System.out.print(" extends " + supercl.getName());
System.out.print("\n{\n");
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println();
printFields(cl);
System.out.println("}");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.exit(0);
}
/**
* Prints all constructors of a class
* @param cl a class
*/
public static void printConstructors(Class cl) {
Constructor[] constructors = cl.getDeclaredConstructors();
for (Constructor c : constructors) {
String name = c.getName();
System.out.print(" ");
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.length() > 0)
System.out.print(modifiers + " ");
System.out.print(name + "(");
// print parameter types
Class[] paramTypes = c.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0)
System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* Prints all methods of a class
* @param cl a class
*/
public static void printMethods(Class cl) {
Method[] methods = cl.getDeclaredMethods();
for (Method m : methods) {
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
// print modifiers, return type and method name
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0)
System.out.print(modifiers + " ");
System.out.print(retType.getName() + " " + name + "(");
// print parameter types
Class[] paramTypes = m.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0)
System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* Prints all fields of a class
* @param cl a class
*/
public static void printFields(Class cl) {
Field[] fields = cl.getDeclaredFields();
for (Field f : fields) {
Class type = f.getType();
String name = f.getName();
System.out.print(" ");
String modifiers = Modifier.toString(f.getModifiers());
if (modifiers.length() > 0)
System.out.print(modifiers + " ");
System.out.println(type.getName() + " " + name + ";");
}
}
}
相關的API:
1)Class 類:
2)Field、Method 和 Constructor 類:
3)Modifier 類:
3 在運行時使用反射分析對象
通過上面的內容,已經知道了如何查看任意對象的數據域名稱和類型:
1.獲取對應的 Class 對象
2.通過 Class 對象調用 getDeclaredFields
那麼數據域的實際內容又怎麼查看呢?
在編寫程序時,如果知道想要查看的域名和類型,查看指定的域是一件很容易的事。利用反射機制可以查看在編譯時還不清楚的對象域。
查看對象域的關鍵方法是 Field 類中的 get
方法。如果 f 是一個 Field 類型的對象,obj 是某個包含 f 域的類的對象,f.get(obj)
將返回一個對象,其值爲 obj 域的當前值。如下:
//Employee
public class Employee{
private String name;
private double salary;
...
}
Employee e = new Employee("zs",1000);
Class c = e.getClass();
try {
Field f = c.getDeclaredField("name"); //獲取 e 對象的 name 域
Object obj = f.get(e);
System.out.println(obj);
} catch (NoSuchFieldException | IllegalAccessException ex) {
ex.printStackTrace();
}
//若 name 是公有域,則成功獲取 zs
實際上,這段代碼存在一個問題:由於 name 域是一個私有域,所以 get
方法將會拋出異常。只有利用get
方法才能得到可訪問域的值。 除非擁有訪問權限,否則 Java 安全機制只允許查看任意對象有哪些域,而不允許讀取它們的值。
反射機制默認行爲受限於 Java 的訪問控制。然而,如果一個 Java 程序沒有受到安全管理器的控制,就可以覆蓋訪問控制。爲了達到這個目的,需要調用 Field、Method 或 Constructor 對象的 setAccessible
方法:
f.setAccessible(true);
f.get(e); //即使 name 域是私有域,也可以獲得 name 域的值
setAccessible
方法是 AccessibleObject 類中的一個方法,它是 Field、Method 或 Constructor 類的公共超類。
get
方法還有一個需要解決的問題:name 域是一個 String ,因此把它作爲 Object 返回沒有什麼問題;但是,若要查看 salary 域,它屬於 double 類型,而 Java 的數值域不是對象,就無法直接查看(拋出異常)。
要想解決這個問題,可以使用 Field 類中的 getDouble
方法,此時,反射機制將會自動地將這個域值打包到相應的對象包裝器中,這裏將打包成 Double。
當然,可以獲得就可以設置。調用 f.set(obj,value)
將 obj 對象的 f 域設置成新值 value。
f.set(e,"lisa"); //將上述的e對象的name域設置爲"lisa"
4 使用反射編寫泛型數組
java.lang.reflect 包中的 Array 類允許動態地創建數組。例如 Array 類的 copyOf
方法:
Employee[] e = new Employee[100];
...
//當數組e已經被填滿後
e = Arrays.copyOf(a,2*a.length);
顯然,copyOf
方法是一個通用的方法,那麼它返回的數組必須爲 Object 數組,那麼它是如何實現的呢?
首先,我們知道,Employee[] 可以臨時地轉換爲 Object[] 數組,然後將它轉換回來也是可以的;但一個一開始就是 Object[] 的數組卻無法轉換成 Employee[] 數組(包括其他類型的數組)。爲了編寫通用的 copyOf
函數,就需要能夠創建與原數組類型相同的新數組。
爲此,就需要 java.lang.reflect 包中的 Array 類的一些方法。其中最關鍵的是 Array 類中的靜態方法 newInstance
,它能構造新數組。在調用它是必須提供兩個參數:原數組的元素類型,新數組的長度。
Object newArray = Array.newInstance(componentType,newLength);
可以通過 Array.getLength(obj)
獲得當前數組 obj 的長度;而要獲取原數組的元素類型以設置新數組的元素類型,就需要進行以下工作:
- 首先獲取原數組的類對象
- 確認它是一個數組
- 使用 Class 類(只能定義表示數組的類對象)的
getComponentType
方法確定數組對應的類型。
copyOf
方法的實現如下:
public static Object copyOf(Object obj,int newlength){
Class c1 = obj.getClass();
if(!c1.isArray())
return null;
Class componentType = c1.getComponentType();
int length = Array.getLength(obj);
Object newArray = Array.newInstance(componentType,newLength);
//public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);原數組,原數組起始位置,目標數組,目標數組開始位置,要copy的數組(原數組的)長度
System.arraycopy(obj,0,newArray,0,Math.min(length,newLength));
return newArray;
}
copyOf
方法可以用來擴展任意類型的數組,而不僅僅是對象數組:
int[] a = {1,2,3,4,5};
a = (int[])copyOf(a,10);
整型數組類型 int[] 可以被轉換成 Object,但不能轉換成對象數組。
5 調用任意方法
反射機制允許 Java 程序員調用任意方法。
在 Method 類中有一個 invoke
方法,它允許調用包裝在當前 Method 對象中的方法,invoke
方法的簽名是:
Object invoke(Object obj,Object... args)
第一個參數是隱式參數,其餘參數是對象提供的顯式參數。
對於靜態方法,第一個參數可以被省略,即可以將它設置爲 null。
例如,假設 m1 代表 Employee 類的 getName
方法,下面的語句顯示瞭如何調用這個方法:
Employee e = new Employee("zs",1000);
Method m1 = null;
Method m2 = null;
try {
m1 = Employee.class.getMethod("getName");
m2 = Employee.class.getMethod("getSalary");
String name = (String) m1.invoke(e);
double s = (Double)m2.invoke(e);
System.out.println(name);
} catch (Exception ex) {
ex.printStackTrace();
}
如果返回類型是基本類型,invoke
方法會返回其包裝器類型。例如,假設有 m2 表示 Employee 的 getSalary
方法,那麼實際返回的對象實際上是一個 Double ,必須相應地完成類型轉換可以使用自動拆箱將它轉換爲一個 double.
getMethod
方法的簽名是:
Method getMethod(String name,Class... parameterTypes)
parameterTypes
是傳入參數的類型。
調用靜態方法時:
Method m = null;
try {
m = Math.class.getMethod("sqrt",double.class);
double x = (Double)m.invoke(null,4);
System.out.println(x); //輸出 2.0
} catch (Exception ex) {
ex.printStackTrace();
}
invoke
的參數和返回值都必須爲 Object 類型的,這意味着必須進行多次的類型轉換。這樣做將會使編譯器錯過檢查代碼錯誤的機會,從而只有在測試階段才能發現這些錯誤。不但如此,使用反射獲得方法指針的代碼要比直接調用方法明顯慢一些。
hod(String name,Class... parameterTypes)
parameterTypes
是傳入參數的類型。
調用靜態方法時:
Method m = null;
try {
m = Math.class.getMethod("sqrt",double.class);
double x = (Double)m.invoke(null,4);
System.out.println(x); //輸出 2.0
} catch (Exception ex) {
ex.printStackTrace();
}
invoke
的參數和返回值都必須爲 Object 類型的,這意味着必須進行多次的類型轉換。這樣做將會使編譯器錯過檢查代碼錯誤的機會,從而只有在測試階段才能發現這些錯誤。不但如此,使用反射獲得方法指針的代碼要比直接調用方法明顯慢一些。
建議僅在有必要的時候才使用 Method 對象,最好使用接口以及 lambda
表達式。