Java反射機制
一、反射機制綜述
在java中,反射是一個功能強大且複雜的機制,許多框架的底層技術和原理都與反射技術有關。因此使用反射技術的主要人員是工具構造者,而不是應用程序員。利用反射機制,我們可以用來:
1.在運行時查看對象
2.在運行時分析類的能力
3.實現通用的數組操作對象
4.利用Method對象,實現類似於C/C++中函數指針的功能
二、通過反射獲取對象
在程序運行期間,Java運行時系統始終爲所有的對象維護一個被稱爲運行時的類型標識。這個信息更蹤着每個對象所屬的類,保存這些信息的類稱爲Class(就是一個類名,沒有其它特殊的含義),Object類中的getClass()方法可以返回一個Class類型的實例。下面我們通過一個例子來進一步理解:
1.創建僱員類
首先定義僱員類,員工信息包括姓名、薪水和僱用日期,包含get方法和提升工資方法
public class Employee {
private String name; //姓名
private double salary; //薪水
private LocalDate hireDay; //僱用日期
public Employee(String name, double salary, int year, int month, int day) {
this.name = name;
this.salary = salary;
this.hireDay = LocalDate.of(year, month, day);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
/**
* 按百分比提升員工工資
* @param byPercent
*/
public void raiseSalary(double byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}
}
2.嘗試獲取Class對象,並對字段進行修改
public class MyTest {
public static void main(String[] args) throws Exception{
Employee e = new Employee("Harry Hacker", 560000, 2012,3,4);
System.out.println(e.getClass().getName() + " " + e.getName());
//獲取Class對象的第一種方法:對象實例調用getClass()方法
Class c1 = e.getClass();
String name = c1.getName();
System.out.println(name);
//獲取Class對象的第二種方法:調用靜態方法forName
String className = "java.util.Random";
Class c2 = Class.forName(className);
System.out.println(c2.getName());
//獲取Class對象的第三種方法:如果T是任意的Java類型,使用T.class
Class c3 = Double[].class;
System.out.println(c3.getName());
//獲取僱員類的name字段,並對它進行修改
Field f = c1.getDeclaredField("name");
//由於是私有域,所以要縣使用setAccessible方法來覆蓋訪問控制
f.setAccessible(true);
//get方法返回的是Object對象,要想正常打印,需要進行類型轉換
Object v = f.get(e);
System.out.println((String) v);
//set方法可以更改對應字段的值
f.set(e, "Tom Smith");
System.out.println((String) f.get(e));
}
}
運行結果:
domain.Employee Harry Hacker
domain.Employee
java.util.Random
[Ljava.lang.Double;
Harry Hacker
Tom Smith
3.代碼解讀
Field類的get方法是查看對象域的關鍵方法,如果f爲Field類型的對象,obj是某個包含f域的類的對象,f.get(obj)將返回一個對象,其值爲obj域的值。由於Employee類中的name是一個私有域,所以如果直接調用get方法會拋出一個IllegalAccessException。因此在調用get方法之前,需要調用setAccessible方法,該方法是AccessibleObject類中的一個方法,這個類是Field、Method和Constructor類的公共父類。還有一點需要注意的是,get方法的返回值是一個Object類型。假定現在要查看salary域,它屬於double類型,是一種數值類型。在Java中,數值類型不算對象。要想解決這個問題,我們可以使用Field類中的getDoule方法,返回值類型爲double。實際上也可以使用get方法,此時,反射機制會自動地將這個域值打包到相應的對象包裝器中,這裏將會打包爲Double。同理,可以用get方法獲取,就可以用set方法更改。
4.應用
當獲取到Class對象之後,可以調用newInstance方法調用默認的構造器(無參構造方法)新建一個對應類的實例。如果該類中沒有無參構造函數,就會拋出一個異常。在Java9之後的版本中,直接用Class對象調用newInstance方法新建對象的方式已經不推薦使用。正確的做法是,先用Class對象調用getConstructor方法獲取對應的構造器,然後再用構造器對象調用newInstance方法。如本案例中,可採用如下方式:
Constructor con = c1.getConstructor(String.class,double.class, int.class,int.class,int.class);
Employee e2 =(Employee)con.newInstance("123",6127,78,7,7);
這種方式有什麼好處呢?在啓動項目時,包含main方法的類被加載。它會加載所需要的類,這些被加載的類又會加載它們需要的類,以此類推。對一個大型應用程序來說,這會使啓動應用程序消耗很多的時間,用戶會因此感到不耐煩。可以使用如下技巧給用戶帶來一種啓動速度很快的錯覺:首先保證包含main方法的類沒有顯示調用其它類,然後一開始顯示一個啓動畫面,通過調研Class.forName手動地加載其它的類。
三、利用反射分析類的能力
在java.lang.reflect包下,有三個非常重要的類叫做Field、Method和Constructor,非別用於描述類的域、方法和構造方法。在這三個類中,有一個叫getName的方法,可以返回對應的名稱。Field類有一個getType方法,用於返回域所屬類型的Class對象。Method類有一個getReturnType方法,用於返回返回值類型。Method和Constructor類有一個方法叫做getParametertypes方法,返回值是一個Object數組。此外,這三個類會員一個叫做getModifiers的方法,它將返回一個整型數值,用不同的位開關描述public和static這樣的修飾符的使用情況。
接下來我們通過一段代碼來理解“通過反射來分析類”:
package reflection;
import java.util.*;
import java.lang.reflect.*;
public class ReflectionTest {
public static void main(String[] args) {
String name;
//從命令行或者用戶輸入來讀取類名
if(args.length > 0)
name = args[0];
else{
Scanner in = new Scanner(System.in);
//輸入的必須是完整類名
System.out.println("Please enter class name(e.g. java.util.Date)");
name = in.next();
}
try {
//如果不爲空,就輸出類名和它的父類
Class c1 = Class.forName(name);
Class superClass = c1.getSuperclass();
String modifiers = Modifier.toString(c1.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print("class "+name);
if (superClass != null && superClass != Object.class ) {
System.out.print("extends "+ superClass.getName());
}
System.out.print("\n{\n");
printConstructors(c1);
System.out.println();
printMethods(c1);
System.out.println();
printFields(c1);
System.out.println("}");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 打印所有的構造函數
* @param c1 一個Class對象
*/
public static void printConstructors(Class c1){
Constructor[] constructors = c1.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 + "(");
//構造方法的參數類型
Class[] papramTypes = c.getParameterTypes();
for (int j = 0; j < papramTypes.length; j++) {
if (j > 0) {
System.out.print(",");
}
System.out.print(papramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* 打印所有的方法
* @param c1
*/
public static void printMethods(Class c1){
Method[] methods = c1.getDeclaredMethods();
for(Method m : methods){
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(retType.getName() + " "+ name + "(");
//打印參數類型
Class[] papramTypes = m.getParameterTypes();
for (int j = 0; j < papramTypes.length; j++) {
if (j > 0) {
System.out.print(",");
}
System.out.print(papramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* 打印類的所有域
* @param c1
*/
public static void printFields(Class c1){
Field[] fields = c1.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 + ";");
}
}
}
1.運行結果
2.代碼分析
Class類中的getFieds、getMethods、 getConstrustors方法分別返回類提供的public域、方法和構造器數組,其中包括父類的公有成員。Class類的getDeclaredFields、getDeclaredMethods和getDeclaredConstrustors方法將分別返回類中聲明的全部域、方法和構造器,包括私有的和受保護的成員,但不包括父類的成員。
如果我們定義一個Manager類,繼承Employee類如下:
public class Manager extends Employee {
private double bonus;
public Manager(String name, double salary, int year, int month, int day){
super(name, salary, year, month, day);
bonus = 0;
}
public double getSalary(){
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
}
運行結果爲:
四、反射構造泛型數組
在Java的Arrays類中,copyOf方法可以用來拓展數組。接下來我們將自定義copyOf方法來實現拓展。一個可行的思路是,首先將所有數組轉換爲Object數組,然後進行拷貝,具體代碼如下:
public static Object[] badCopyOf(Object[] a, int newLength){
Object[] newArray = new Object[newLength];
System.arraycopy(a, 0 , newArray, 0, Math.min(a.length, newLength));
return newArray;
}
但是在實際使用時會產生一個問題,這段代碼的返回對象是一個對象數組(Object[])類型,一個對象數組不能轉換成其它類型。例如在對僱員數組(Employee[])進行拷貝時,會產生ClassCastExceptoion異常。這是因爲,new分配的空間就是Object類型。將一個Employee[]臨時轉換成Object[]數組時,然後轉換回來可以的。但是無法將一個一開始就是Object[]類型的數組轉換成Employee[]數組。因此,我們需要改進一下我們的思路:
1.獲取a數組的類對象
2.確認它是一個數組
3.使用Class類的getComponentType方法確定數組對應的類型。
具體代碼如下:
public static Object goodCopyOf(Object a, int newLength){
Class c1 = a.getClass();
if(!c1.isArray())
return null;
Class componentType = c1.getComponentType();
int length = Array.getLength(a);
Object newArray = Array.newInstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
return newArray;
}
注意:爲了實現對數值類型數組的支持,例如int[],goodCopyOf的參數應該是Object類型,而不是對象型數組Object[]。整型數組類型int[]可以轉換爲Object,但是不能轉換爲對象數組。測試代碼及結果如下:
public static void main(String[] args) {
int[] a = {1,2,3};
a = (int [])goodCopyOf(a,10);
System.out.println(Arrays.toString(a));
String[] b = {"Tom","Dick","Harry"};
b = (String[]) goodCopyOf(b,10);
System.out.println(Arrays.toString(b));
System.out.println("The following call will generate an exception.");
b = (String[]) badCopyOf(b,10);
}
五、反射實現調用任意方法
在C/C++中,可以用函數指針執行任意函數。Java雖然沒有提供方法指針,但是可以提供反射機制來實現類似的功能。在Method類中有一個invoke方法,它允許調用包裝在當前Method對象中的方法。invoke方法的簽名爲:Object invoke(Object obj, Object... args),第一個參數是調用方法的對象,其餘的提供了調用方法所需要的參數。對於靜態方法,第一個參數可以被忽略,即直接設置爲null。例如,在我們的案例中,可以如下使用:
Employee e = new Employee("Harry Hacker", 560000, 2012,3,4);
System.out.println(e.getClass().getName() + " " + e.getName());
//獲取Class對象的第一種方法:對象實例調用getClass()方法
Class c1 = e.getClass();
String name = c1.getName();
System.out.println(name);
Method m1 = c1.getMethod("getName");
String resultName = (String) m1.invoke(e);
System.out.println(resultName);
這樣就可以調用Employee類中的getName方法。
對於invoke方法的第二個參數,有兩種傳值方式:
Manager manager = new Manager("Bob Black", 1230, 2014,4,4);
System.out.println(manager);
Class managerClass = manager.getClass();
Constructor constructor = managerClass.getConstructor(String.class,double.class, int.class,int.class,int.class);
Manager manager1 =( Manager) constructor.newInstance("123",6127,1978,7,7);
System.out.println(manager1);
Method method = managerClass.getMethod("setBonus", double.class, boolean.class);
//invoke傳參數的方法一:傳入Object數組
Object[] obj = { 123, true};
method.invoke(manager, obj);
System.out.println(manager.getSalary());
//invoke傳參數的方法二:直接傳入值
method.invoke(manager, 63767, false);
System.out.println(manager.getSalary());
接下來的這個例子,顯示了一個打印諸如Math.Sqrt、Math.Sin這樣的數學函數值表的程序,這些由於都是static方法,因此第一個參數都是null。
public class MethodTableTest {
public static void main(String[] args) throws Exception{
Method square = MethodTableTest.class.getMethod("square", double.class);
Method sqrt = Math.class.getMethod("sqrt", double.class);
printTable(1,10,10,square);
printTable(1,10,10,sqrt);
}
public static double square(double x) {return x*x;}
public static void printTable(double from, double to, int n, Method f){
System.out.println(f);
double dx = (to - from)/(n-1);
for(double x = from; x <= to; x += dx){
try {
double y = (Double) f.invoke(null ,x);
System.out.printf("%10.4f | %10.4f%n" , x, y);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
上述程序表明,可以使用Method對象實現C語言中函數指針的所有操作,這種程序設計風格並不簡單,出錯的可能性比較大。如果在調用方法的時候提供了錯誤的參數,那麼invoke方法就會拋出異常。另外invoke的參數和返回值必須是Object類型的,這就意味着必須進行多次的類型轉換。但是這樣做會使編譯器錯過類型檢查的機會。只有等到測試階段纔會發現這些錯誤,這使得代碼維護和修改變得更加困難。除此之外,使用反射獲得方法指針的代碼要比直接調用方法更慢一些。
鑑於上述原因,僅在必要的時候才使用Method對象,最好使用接口和lambda表達式來實現類似的功能。