Java——反射

什麼是反射?

答:能夠分析類能力的程序稱爲反射(reflective)。

反射機制可以用來幹嘛?

答:1.在運行時分析類的能力       2.在運行時查看對象,例如toString方法。 3.實現通用的數組操作代碼。4.利用Method對象。

在程序運行期間,Java運行時系統始終爲所有的對象維護一個稱爲運行時的類型標識。這個信息跟蹤着每個對象所屬的類。虛擬機利用運行時類型信息選擇相應的方法執行。我們可以通過專門的Java類去訪問這些信息,保存這些信息的類稱爲Class。

獲取Class對象有三種方法:

1.通過對象.getClass()方法獲得。 

Class c1 = new String("getClass方法").getClass();

2.通過靜態方法forName(String className)獲得className類名對應的Class對象。注意,只有在className是類名或接口名時才能使用這個方法,並且無論何時使用這個方法,都必須提供一個異常處理器。

String className = "java.util.Random";
try {
    Class c1 = Class.forName(className);
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

3.使用T.class,如果T是任意的Java類型(或void關鍵字),T.class將代表匹配的類對象。

Class c1 = Random.class;
Class c2 = int.class;
Class c3 = Double[].class;
Class c4 = void.class;

需要注意的是一個Class對象實際上表示的是一個類型,而這個類型不一定是一種類。例如,int,void不是類,但是int.class是一個Class對象。其實Class類實際上是一個泛型類。例如Random.class的類型是Class<Random>。

注意,獲取類對象的時候,會導致類屬性被初始化。無論什麼途徑獲取類對象,都會導致靜態屬性被初始化,而且只會執行一次。(除了直接使用 Class c = T.class 這種方式,這種方式不會導致靜態屬性被初始化)

public class TestReflection {

    public static void main(String[] args) throws InterruptedException {
        Class c = Test.class;

    }
}

class Test {
    static String test;

    static {
        System.out.println("靜態方法執行");
        test = "類屬性初始化成功";
        System.out.println("test: " + test);
    }
}

public class TestReflection {

    public static void main(String[] args) throws InterruptedException {
        Class c = Test.class;
        Class c1 = new Test().getClass();
    }
}

class Test {
    static String test;

    static {
        System.out.println("靜態方法執行");
        test = "類屬性初始化成功";
        System.out.println("test: " + test);
    }
}

虛擬機爲每個類型管理一個Class對象。因此可以使用"=="運算符來比較兩個對象。例如:

public class TestReflection {

    public static void main(String[] args) throws InterruptedException, ClassNotFoundException {
        Class c = Test.class;
        Class c1 = new Test().getClass();
        if(c == c1){
            System.out.println("相等");
        }
    }
}

class Test {
    static String test;

    static {
        System.out.println("靜態方法執行");
        test = "類屬性初始化成功";
        System.out.println("test: " + test);
    }
}

Class對象中有一個很有用的方法---->newInstance(),這個方法可以用來動態地創建一個類的實例。例如,test.getClass().newInstance();創建了一個與test具有相同類型的實例。newInstace方法調用默認的構造器(沒有參數的構造器)初始化新創建的對象。如果這個類沒有默認的構造器,就會拋出一個異常。通常我們將forName與newInstance配合起來使用。

注意:如果希望向按名稱創建的類的構造器提供參數,就必須使用Constructor類中的newInstance方法。

分割線---------------------------------------------------------------------------------------------------------------------------

下面簡要的介紹一下反射機制最重要的內容——檢查類的結構。

咋java.lang.reflect包中有三個類Filed、Method和Constructor分別用來描述類的域、方法和構造器。這三個類都有一個叫做getName的方法,用來返回項目的名稱,一個getModifiers的方法,返回一個整形數值,用不同的位開關描述public和static這樣的修飾符使用狀況。Filed類有一個getType方法,用來返回描述域所屬類型的Class對象。Method 和 Constructor 類有能夠報告參數類型的方法,Method 類還有一個可以報告返回類型的方法。

Class類中的getFileds、getMethods 和 getConstructors方法將返回類提供的public域、方法和構造器數組,其中包括超類的公有成員。Class類的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors方法將返回類中聲明的全部域、方法和構造器,其中包括私有和受保護成員,但不包括超類的成員。

下面程序顯示瞭如何打印一個類的全部信息的方法。輸入要打印的類名即可,注意如果前面有包名的話,也要輸入。

package 反射;

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

/**
 * @ClassName: TestReflection
 * @Description: 打印類的全部信息
 * @author: GGBOY
 * @date 2019/10/15 21:19
 * @Version: 1.0
 **/
public class TestReflection {

    public static void main(String[] args) {
        // read class name from command line args user input
        String name;
        Scanner in = new Scanner(System.in);
        System.out.println("Enter class name (e.g 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();
        }
    }

    /**
     * 打印類的所有構造器
     *
     * @param c1 a class
     * @return void
     * @author GGBOY
     * @date 2019/10/15
     */
    public static void printConstructors(Class c1) {
        // getDeclaredConstructors方法返回這個類的所有構造器。
        // getConstructors方法返回這個類的公有構造器。
        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[] paramTypes = c.getParameterTypes();
            for (int i = 0; i < paramTypes.length; i++) {
                if (i > 0) {
                    System.out.print(", ");
                }
                // 打印構造器中的參數類型
                System.out.print(paramTypes[i].getName());
            }
            System.out.println(");");
        }
    }

    /**
     * 打印類的所有方法,包括實現接口的全部方法,但不包括由父類繼承的方法
     *
     * @param cl 一個類
     * @return void
     * @author GGBOY
     * @date 2019/10/16
     */
    public static void printMethods(Class cl) {
        // getDeclaredMethods 返回這個類或接口的全部方法,但不包括由超類繼承了的方法。
        // getMethods會返回所有的公有方法,包括從超類繼承來的方法。
        Method[] methods = cl.getDeclaredMethods();

        for (Method method :
                methods) {
            // 獲取方法的返回類型
            Class retType = method.getReturnType();
            // 獲取方法名
            String name = method.getName();

            System.out.print(" ");
            String modifiers = Modifier.toString(method.getModifiers());
            if (modifiers.length() > 0) {
                // 打印修飾符
                System.out.print(modifiers + " ");
            }
            // 打印返回類型和方法名
            System.out.print(retType.getName() + " " + name + "(");

            // 獲取方法的參數
            Class[] paramTypes = method.getParameterTypes();
            for (int i = 0; i < paramTypes.length; i++) {
                if (i > 0) {
                    System.out.print(", ");
                }
                System.out.print(paramTypes[i].getName());
            }
            System.out.println(");");
        }
    }

    /**
     * 打印類的所有屬性,包括父類的公有屬性
     *
     * @param cl 一個類
     * @return void
     * @author GGBOY
     * @date 2019/10/16
     */
    public static void printFields(Class cl) {
        // getDeclaredFields方法返回包含Field對象的數組,這些對象記錄了類的全部域。
        // getFields方法將返回一個包含Field對象的數組,這些對象記錄了這個類或其超類的公有域。
        // 如果類中沒有域,或者Class對象描述的是基本類型或數組類型,這些方法將返回一個長度爲0的數組
        Field[] fields = cl.getDeclaredFields();

        for (Field field :
                fields) {
            // 獲取屬性類型
            Class type = field.getType();
            // 獲取屬性名
            String name = field.getName();
            System.out.print(" ");
            String modifiers = Modifier.toString(field.getModifiers());
            if (modifiers.length() > 0) {
                System.out.print(modifiers + " ");
            }
            System.out.println(type.getName() + " " + name + ";");
        }
    }
}


通過前面的講解,知道了如何查看任意對象的數據域名稱和類型:通過獲得對於的Class對象,再通過Class對象調用getDeclaredFieds。

那麼如何查看數據域中的內容呢

答:查看對象域的關鍵方法是Field類中的get方法。如果f是一個Field類型的對象(例如,通過getDeclaredFields得到的對象),obj是某個包含f域的類的對象,f.get(obj)將返回一個對象,其值爲obj域的當前值。例如:

public class ReflectionTest {

    public static void main(String[] args) {
        Test t = new Test("xxx", 222.22);
        Class cl = t.getClass();
        Field f = null;
        try {
            // 獲得Test類的name域
            f = cl.getDeclaredField("name");
            // 取出name域中的值
            Object v = f.get(t);
            // 打印
            System.out.println(v.toString());
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

    }

}

class Test {
    private String name;
    private double salary;

    public Test(String name, double salary) {
        this.salary = salary;
        this.name = name;
    }

}

實際上,上述代碼有一個問題。由於name是一個私有域,所以get 方法會拋出一個IllegalAccessException異常。只有利用get方法才能得到可訪問域的值。除非擁有訪問權限,否則Java安全機制只允許查看任意對象有哪些域,而不允許讀取它們的值。

我們可以通過 f.setAccessible(true)方法來得到訪問權限,這樣我們就可以通過f.get(t)獲得name域的值了。注意:get方法還有一個需要解決的問題。name域是一個String類型,所以把它作爲Object返回沒有什麼問題。但是,假定我們想要查看的是salary域,它屬於double類型,而Java中數值類型不是對象。要想要解決這個問題,可以使用Field類中的getDouble方法,這樣反射機制將會自動的將這個域值打包到相應的對象包裝器中,即打包成Double.

當然,可以獲得就可以設置。通過調用f.set(obj, value)可以將obj對象的f域設置成新的value值。

分割線---------------------------------------------------------------------------------------------------------------------------

使用反射編寫泛型數組:

java.lang.reflect包中的Array類允許動態的創建數組,例如:

Test[] t = new Test[100];
...
// t 數組已經滿了
t = Arrays.copyOf(t, 2 * t.length);

copyOf方法可以用於擴展已經填滿的數組。那麼如何編寫一個方法,正好能將Test[]數組轉換爲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;
    }

這個方法咋一看好像是可以的,但是實際上存在一個問題,因爲這個方法返回的是對象數組類型,這是由於使用了 new Object[newLength]代碼創建的數組。一個對象數組不能轉換成Test數組,如果這樣做的話,在運行時會拋出ClassCastException異常。將一個Test[]臨時轉換成Object[]數組,再把它轉換回來是可以的,但是一個從一開始就是Object[]的數組卻永遠不能轉換成Test[]數組。

爲了編寫通用的代碼,需要能夠創建與原數組類型相同的新數組,此時我們就可以使用Array類中的靜態方法newInstance,它能構造新數組。在調用它時必須提供兩個參數,一個是數組的元素類型,一個是數組的長度.

Object newArray = Array.newInstance(componentType, newLength);

我們可以通過調用Array.getLength(a)獲得數組的長度。想要獲得新數組元素類型時,就需要這樣做了:1.首先獲得數組a的類對象 2.確認它是一個數組。3.使用Class類的getComponentType方法確定數組對應的類型。下面是新的拷貝方法:

public static Object goodCopyOf(Object a, int newLength) {
        Class cl = a.getClass();
        // 如果傳進來的參數不是數組類型
        if (!cl.isArray()) {
            return null;
        }
        Class componentType = cl.getComponentType();
        int length = Array.getLength(a);
        Object newArray = Array.newInstance(componentType, newLength);
        System.arraycopy(a, 0, newArray, 0,
                Math.min(length, newLength));
        return newArray;
    }

這個CopyOf方法可以用來擴展任意類型的數組,而不僅僅是對象數組。在這個方法中,將參數聲明爲Object類型是因爲,數組類型xx[]可以轉換爲Object,而不能轉換成對象數組。下面是兩個擴展數組方法的測試程序,需要注意的是badCopyOf的返回值進行類型轉換時會拋出一個異常。

package 反射;

import java.lang.reflect.Array;
import java.util.Arrays;

/**
 * @ClassName: CopyOfTest
 * @Description: 擴展任意類型數組方法的測試類
 * @author: GGBOY
 * @date 2019/10/16 14:16
 * @Version: 1.0
 **/
public class CopyOfTest {

    public static void main(String[] args) {
        int[] a = {1, 2, 3};
        a = (int[]) goodCopyOf(a, 10);
        System.out.println(Arrays.toString(a));

        String[] b = {"小米", "紅米", "華爲"};
        b = (String[]) goodCopyOf(b, 10);
        System.out.println(Arrays.toString(b));

        System.out.println("下面的擴展方法在進行類型轉換時會報錯");
        b = (String[]) badCopyOf(b, 10);
    }

    /**
     * 擴充數組
     *
     * @param a         需要擴展的數組
     * @param newLength 新數組的長度
     * @return java.lang.Object[]
     * @author GGBOY
     * @date 2019/10/16
     */
    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;
    }

    /**
     * 通過創建相同類型的數組來擴充數組
     *
     * @param a         需要擴充的原數組
     * @param newLength 新數組的長度
     * @return java.lang.Object
     * @author GGBOY
     * @date 2019/10/16
     */
    public static Object goodCopyOf(Object a, int newLength) {
        Class cl = a.getClass();
        // 如果傳進來的參數不是數組類型
        if (!cl.isArray()) {
            return null;
        }
        Class componentType = cl.getComponentType();
        int length = Array.getLength(a);
        // newInstance(Class componentType, int[] lengths) 返回一個具有給定類型、給定維數的新數組
        // newInstance(Class componentType, int length)
        Object newArray = Array.newInstance(componentType, newLength);
        System.arraycopy(a, 0, newArray, 0,
                Math.min(length, newLength));
        return newArray;
    }
}

分割線---------------------------------------------------------------------------------------------------------------------------

使用反射調用類中的任意方法

在Method類中有一個invoke方法,它允許調用包裝在當前Method對象中的方法。invoke方法的聲明是:Object invoke(Object obj, Object... args) 第一個參數是隱士參數,其餘的對象提供了顯示參數。

對於靜態方法而言,第一個參數可以被忽略,即可以將它設置爲null。例如,假設用tl 代碼Test類的getName方法,下面這條語句顯示瞭如何調用這個方法:

String n = (String) tl.invoke(t);

我們可以通過調用getDeclareMethods方法,對返回的Method對象數組進行查找,搜索我們想要的方法。當然也可以通過調用Class類中的getMethod方法得到想要的方法。getMethod方法聲明如下:

Method getMethod(String name, Class... parameterType)

如果類中有同名方法的話,使用getMethod方法時,要注意parameterType參數的匹配。例如:

public class Test {

    public static void main(String[] args) throws Exception {
        Hero hero = new Hero();
        Class cl = hero.getClass();
        Method method = cl.getMethod("method", 
                String.class);
        method.invoke(hero, "this");
    }

}

class Hero {

    public void method() {
        System.out.println("這是無參方法一");
    }

    public void method(String s) {
        System.out.println("這是有參數方法,參數是: " + s);
    }
}

 

注意:如果返回的是基本類型,invoke方法會返回其包裝器類型。invoke的參數和返回值必須是Object類型,所以必須進行多次類型轉換,並且這樣做的話,編譯器無法檢查代碼,要等到運行測試階段纔會發現這些錯誤。

 

反射介紹到這裏就完了,如果有什麼錯誤,請指出來,不勝感激。----菜雞程序員

 

參考資料:

   《java核心技術 卷I》

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