java反射機制

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.運行結果

解析Employee類的結果

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;
    }

}

運行結果爲:

解析Manager類的結果

四、反射構造泛型數組

在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表達式來實現類似的功能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章