1. 程式人生 > >Java核心技術再理解——Java反射機制

Java核心技術再理解——Java反射機制

在Java中反射是一種十分強大的機制,可以傳遞class,可以取得一個類的所有資訊,動態的生成一個類。甚至可以取得一個類的父類。同樣反射這一機制也是Java高階開發所必須掌握的。
能夠分析類能力的程式被稱為反射。本文將談到java.lang.Class,以及java.lang.reflect中的Method、Field、Constructor等等。

Class類

凡是接觸過Java的,甚至是知道Java的程式設計師都知道Java是一門純面向物件的語言。在Java中可以說萬事萬物皆物件。甚至是程式設計師自己編寫的任意一個類。這些類是java.lang.Class的例項物件。我們可以通過Class類來訪問一個特定類的資訊。

        Date date1 = new Date();
        Class c1 = date1.getClass();//getClass()返回date1的class type

如果輸出c1,我們會在控制檯看到:

class java.util.Date

我們可以清晰的看出,date1是一個class type 為class的一個java.util.Date物件。
如果類名是儲存在一個字串中,並且在執行的時候可能發生改變,那麼我們可以呼叫靜態方法forName來獲取對應的Class物件。這個方法只有在所提供的字串是完整的類名或者介面名的時候才能使用,否則將會丟擲checkedException。

        String className="java.util.Date";
        try {
            Class c2=Class.forName(className);//forName返回給定字串的類或者介面的名稱以及class type
            System.out.println(c2);
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

獲取Class物件的第三種方法十分的簡單。如果T是任意的Java型別。那麼T.class將代表匹配的類物件。

        Class c4=Date.class;
        Class c5= int[].class;

還有一個很有用的方法newInstance可以用來快速的建立一個類的例項。所以我們可以用forName和newInstance相結合根據一個儲存在字串中的類名來建立一個物件。

String className="java.util.Date";
 Object o;
        try {
            o = Class.forName(className).newInstance();
            //forName和newInstance都是返回Class的。注意強制型別轉換
            Date date= (Date) Class.forName(className).newInstance();
        } catch (InstantiationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

利用反射分析類

在java.lang.reflection包中有三個類Field,Method,Constructor分別描述類的域,方法和構造器。它們都有一個叫做getName的方法用來返回專案的名稱(包括包名)。通過呼叫這三個類中的一些方法,程式設計師很容易就能知道一個類的結構。
下面的程式碼就顯示瞭如何列印一個類的全部資訊的方法。

package org.joea.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;

/**
 * 利用反射來顯示一個類的詳細資訊
 * 
 * @author Joea
 * 
 */
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(eg:java.util.Date):");
            name = in.next();
        }
        try {
            Class c = Class.forName(name);
            Class superc = c.getSuperclass();
            String modifier = Modifier.toString(c.getModifiers());// 獲取修飾符
            if (modifier.length() > 0)
                System.out.print(modifier + " ");
            System.out.print("class " + c.getName());
            if (superc != null && superc != Object.class)
                System.out.print(" extends " + superc.getName());
            System.out.println("{");
            printFiles(c);
            printConstructors(c);
            printMethods(c);
            System.out.println("}");
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * 獲取類的所有的構造方法
     * 
     * @param c
     */
    public static void printConstructors(Class c) {
        Constructor[] constructor = c.getDeclaredConstructors();// 返回包含該類宣告的所有構造方法的陣列,不包括從父類繼承而來的方法
        for (Constructor constructor2 : constructor) {
            String name = constructor2.getName();
            System.out.print("  ");
            String modifiers = Modifier.toString(constructor2.getModifiers());// 獲取方法修飾符
            if (modifiers.length() > 0)
                System.out.print(modifiers + " ");
            System.out.print(name + "(");
            Class prameterTypes[] = constructor2.getParameterTypes();// 返回該方法所有引數的陣列
            for (Class p : prameterTypes) {
                System.out.print(p.getName() + ",");
            }
            System.out.println(")");

        }
    }

    /**
     * 列印類中所有的方法,不包括從父類繼承的方法
     * 
     * @param c
     */
    public static void printMethods(Class c) {
        Method methods[] = c.getDeclaredMethods();// 返回包含該類宣告的所有方法的陣列,不包括從父類繼承而來的方法
        for (Method method : methods) {
            System.out.print("  ");
            String modifier = Modifier.toString(method.getModifiers());
            if (modifier.length() > 0)
                System.out.print(modifier + " ");
            String methodName = method.getName();
            System.out.print(methodName + "(");
            Class prameterType[] = method.getParameterTypes();
            for (Class prameter : prameterType) {
                System.out.print(prameter.getName() + ",");
            }
            System.out.println(")");
        }
    }

    /**
     * 列印類中的所有成員變數,不包括從父類繼承而來的
     * 
     * @param c
     */

    public static void printFiles(Class c) {
        Field[] fields = c.getDeclaredFields();// 返回包含該類宣告的所有成員變數的陣列,不包括從父類繼承而來的方法
        for (Field field : fields) {
            Class type = field.getType();
            String name = field.getName();
            System.out.print("  ");
            String modifier = Modifier.toString(field.getModifiers());
            if (modifier.length() > 0)
                System.out.print(modifier + " ");
            System.out.println(type.getName() + " " + name + ";");
        }
    }
}

利用反射了解泛型的本質

通過上面的介紹,讀者們肯定都已經知道了如何檢視任意物件的域名和class type。在介紹泛型本質的之前。我們來看一下以下程式碼:

package org.joea.test;

import java.util.ArrayList;

public class SimpleTest {
    public static void main(String[] args) {

        ArrayList list1=new ArrayList();
        ArrayList<String> list2=new ArrayList();
        Class c1=list1.getClass();
        Class c2=list2.getClass();
        System.out.println(c1==c2);
    }

}

執行上面的程式碼,會發現返回的是true。其實筆者剛開始遇到這個問題的時候十分困惑。如果你向list2中傳遞一個String的話,編譯會正常通過。如果傳遞一個非String的值,那麼編譯就會報錯。所以就理所當然的人文list2的class type是String。當時轉念想一想,如果list2的class type是String,那麼list1的是什麼。經過驗證發現list1和list2的class type都是class java.util.ArrayList

抱著這個疑問查閱了一些質料。得到結果是.:Java範型時編譯時技術,在執行時不包含範型資訊,僅僅Class的例項中包含了型別引數的定義資訊。泛型是通過java編譯器的稱為擦除(erasure)的前端處理來實現的。你可以(基本上就是)把它認為是一個從源
碼到原始碼的轉換,它把泛型版本轉換成非泛型版本。
基本上,擦除去掉了所有的泛型型別資訊。所有在尖括號之間的型別資訊都被扔掉了,因此,比如說一個
List型別被轉換為List。所有對型別變數的引用被替換成型別變數的上限(通常是Object)。而且,
無論何時結果程式碼型別不正確,會插入一個到合適型別的轉換。
T badCast(T t, Object o) {
return (T) o; // unchecked warning
}
型別引數在執行時並不存在。這意味著它們不會新增任何的時間或者空間上的負擔,這很好。不幸的是,這也意味著你不能依靠他們進行型別轉換。這樣也就解釋了為什麼上面那段程式碼的結果是true了。

利用反射呼叫任意方法

學過C/C++的讀者應該知道,由於函式指標的存在,使得在C/C++中可以從函式指標執行任意函式。Java中沒有提供指標,但是反射機制允許程式設計師呼叫任意方法。

在Method類中存在一個invoke方法,該方法簽名如下:

    Object incoke(Object obj,Object...args)

第一個引數是隱式引數。對於靜態方法第一個引數可一忽略或者設定為null。
如果返回值是基本的資料型別,invoke方法將返回其包裝器型別。否則,則需要進行相應的型別轉換。
那麼,如何得到Method物件呢?前面介紹過通過呼叫getDeclareMethods方法,然後通過對返回的Method陣列進行查詢。這種方法繁瑣並且效率低。程式設計師也可以呼叫Class類中的getMethod方法得到想要的方法。該方法簽名如下:

    Method getMethod(String name,Object...ParameterTypes)

這樣我們就可以通過給getMethod傳遞方法名,方法的引數型別來找到我們需要的唯一方法。

package org.joea.methodreflect;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MethodReflectionTest {
    public static void main(String[] args) throws NoSuchMethodException, SecurityException {
        Method sqrt=Math.class.getMethod("sqrt", double.class); //獲取對應的方法
        Method square=MethodReflectionTest.class.getMethod("square", double.class);

        Print(0, 10, 10, sqrt);
        Print(0,10,10,square);
    }

    public static double square(double x){
        return x*x;
    }

    public static void Print(double from,double to,double row,Method method){

        System.out.println(method.getName());

        for(double x=from;x<=to;x++){
            double y;
            try {
                y = (double) method.invoke(null, x);//method為static方法,不屬於任何一個對像。所以第一個引數為null
                System.out.printf("%10.4f | %10.4f %n",x,y);
            } catch (IllegalAccessException | IllegalArgumentException
                    | InvocationTargetException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }

    }

}

上面的程式清楚的顯示了,在Java中可以通過Method物件實現C/C++中的函式指標的所有操作。但是invoke方法的返回值必須是Object,這就意味著必須進行多次型別轉換。這樣可能會產生意想不到的錯誤。所以在開發中儘量不要使用Method方法的回撥。利用介面進行回撥是一個良好的編碼風格。