Java通過反射操作泛型

2020-07-16 10:05:18
學習完《Java泛型詳解》一節中我們了解到,從Java 1.5 版本開始,Java 的 Class 類增加了泛型功能,從而允許使用泛型來限制 Class 類。例如,String.class 的型別實際上是 Class<String>。如果 Class 對應的類暫時未知,則使用 Class<?>。通過在反射中使用泛型,可以避免使用反射生成的物件需要強制型別轉換。

泛型和 Class 類

使用 Class<T> 泛型可以避免強制型別轉換。例如,下面提供一個簡單的物件工廠,該物件工廠可以根據指定類來提供該類的範例。
public class ObjectFactory {
    public static Object getInstance(String clsName) {
        try {
            // 建立指定類對應的Class物件
            Class cls = Class.forName(clsName);
            // 返回使用該Class物件建立的範例
            return cls.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}
上面程式中第 5 、7 行程式碼根據指定的字串型別建立了一個新物件,但這個物件的型別是 Object,因此當需要使用 ObjectFactory 的 getInstance() 方法來建立物件時,程式碼如下:

// 獲取範例後需要強制型別轉換
Date d = (Date)ObjectFactory.getInstance("java.util.Date");

JFrame f = (JFrame)ObjectFactory .getInstance("java.util.Date");

上面程式碼在編譯時不會有任何問題,但執行時將丟擲 ClassCastException(強制型別轉換異常),因為程式試圖將一個 Date 物件轉換成 JFrame 物件。

如果將上面的 ObjectFactory 工廠類改寫成使用泛型後的 Class,就可以避免這種情況。
public class ObjectFactory2 {
    public static <T> T getInstance(Class<T> cls) {
        try {
            return cls.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) {
        // 獲取範例後無須型別轉換
        Date d = CrazyitObjectFactory2.getInstance(Date.class);
        JFrame f = CrazyitObjectFactory2.getInstance(JFrame.class);
    }
}
在上面程式的 getInstance() 方法中傳入一個 Class<T> 引數,這是一個泛型化的 Class 物件,呼叫該 Class 物件的 newInstance() 方法將返回一個 T 物件,如程式中第 4 行程式碼所示。接下來當使用 ObjectFactory2 工廠類的 getInstance() 方法來產生物件時,無須使用強制型別轉換,系統會執行更嚴格的檢查,不會出現 ClassCastException 執行時異常。

前面介紹使用 Array 類來建立陣列時,曾經看到如下程式碼:

// 使用Array的newInstance方法來建立一個陣列
Object arr = Array.newInstance(String.class, 10);

對於上面的程式碼其實使用並不是非常方便,因為 newInstance() 方法返回的確實是一個 String[] 陣列,而不是簡單的 Object 物件。如果需要將 arr 物件當成 String[] 陣列使用,則必須使用強制型別轉換,但是這是不安全的操作。

奇怪的是,Array 的 newInstance() 方法簽名(方法簽名由方法名稱和參數列組成)為如下形式:

public static Object newInstance(Class<?> componentType, int... dimensions)

在這個方法簽名中使用了 Class<?> 泛型,但並沒有真正利用這個泛型。如果將該方法簽名改為如下形式:

public static <T> T[] newInstance(Class<T> componentType, int length)

這樣就可以在呼叫該方法後無需強制型別轉換了。不過,這個方法暫時只能建立一維陣列,也就是不能利用可變個數的引數優勢了。

為了示範泛型的優勢,可以對 Array 的 newInstance() 方法進行包裝。
public class CrazyitArray {
    // 對Array的newInstance方法進行包裝
    @SuppressWarnings("unchecked")
    public static <T> T[] newInstance(Class<T> componentType, int length) {
        return (T[]) Array.newInstance(componentType, length); 
    }

    public static void main(String[] args) {
        // 使用 CrazyitArray 的 newInstance()建立一維陣列
        String[] arr = CrazyitArray.newInstance(String.class, 10);
        // 使用 CrazyitArray 的 newInstance()建立二維陣列
        // 在這種情況下,只要設定陣列元素的型別是int[]即可
        int[][] intArr = CrazyitArray.newInstance(int[].class, 5);
        arr[5] = "C語言中文網Java教學";
        // intArr是二維陣列,初始化該陣列的第二個陣列元素
        // 二維陣列的元素必須是一維陣列
        intArr[1] = new int[]{ 23, 12 };
        System.out.println(arr[5]);
        System.out.println(intArr[1][1]);
    }
}
上面程式中第 4、5、6、10 和 13 定義的 newInstance() 方法對 Array 類提供的 newInstance() 方法進行了包裝,將方法簽名改成了 public static <T> T[] newInstance(Class<T> componentType, int length),這就保證程式通過該 newInstance() 方法建立陣列時的返回值就是陣列物件,而不是 Object 物件,從而避免了強制型別轉換。

提示:@SuppressWarnings("unchecked") 告訴編譯器忽略 unchecked 警告資訊,如使用 List,ArrayList 等未進行引數化產生的警告資訊。程式在第 5 行程式碼處將會有一個 unchecked 編譯警告,所以程式使用了 @SuppressWarnings 來抑制這個警告資訊。

使用反射來獲取泛型資訊

通過指定類對應的 Class 物件,可以獲得該類裡包含的所有成員變數,不管該成員變數是使用 private 修飾,還是使用 public 修飾。獲得了成員變數對應的 Field 物件後,就可以很容易地獲得該成員變數的資料型別,即使用如下程式碼即可獲得指定成員變數的型別。

// 獲取成員變數 f 的型別
Class<?> a = f.getType();

但這種方式只對普通型別的成員變數有效。如果該成員變數的型別是有泛型型別的型別,如 Map<String, Integer>型別,則不能準確地得到該成員變數的泛型引數。

為了獲得指定成員變數的泛型型別,應先使用如下方法來獲取該成員變數的泛型型別。

// 獲得成員變數f的泛型型別
Type gType = f.getGenericType();

然後將 Type 物件強制型別轉換為 ParameterizedType 物件,ParameterizedType 代表被引數化的型別,也就是增加了泛型限制的型別。ParameterizedType 類提供了如下兩個方法。
  • getRawType():返回沒有泛型資訊的原始型別。
  • getActualTypeArguments():返回泛型引數的型別。

下面是一個獲取泛型型別的完整程式。
public class GenericTest {
    private Map<String, Integer> score;

    public static void main(String[] args) throws Exception {
        Class<GenericTest> clazz = GenericTest.class;
        Field f = clazz.getDeclaredField("score");
        // 直接使用getType()取出型別只對普通型別的成員變數有效
        Class<?> a = f.getType();
        // 下面將看到僅輸出java.util.Map
        System.out.println("score 的型別是:" + a);
        // 獲得成員變數f的泛型型別
        Type gType = f.getGenericType();
        // 如果 gType 型別是 ParameterizedType物件
        if (gType instanceof ParameterizedType) {
            // 強制型別轉換
            ParameterizedType pType = (ParameterizedType) gType;
            // 獲取原始型別
            Type rType = pType.getRawType();
            System.out.println("原始型別是:" + rType);
            // 取得泛型型別的泛型引數
            Type[] tArgs = pType.getActualTypeArguments();
            System.out.println("泛型資訊是:");
            for (int i = 0; i < tArgs.length; i++) {
                System.out.println("第" + i + "個泛型型別是:" + tArgs[i]);
            }
        } else {
            System.out.println("獲取泛型型別出錯!");
        }
    }
}
上面程式中的第 12、16、18 和 21 行程式碼就是取得泛型型別的關鍵程式碼。執行上面程式,將看到如下執行結果:

score 的型別是:interface java.util.Map
原始型別是:interface java.util.Map
泛型資訊是:
第0個泛型型別是:class java.lang.String
第1個泛型型別是:class java.lang.Integer

從上面的執行結果可以看出,使用 getType() 方法只能獲取普通型別的成員變數的資料型別。對於增加了泛型的成員變數,應該使用 getGenericType() 方法來取得其型別。

提示:Type 也是 java.lang.reflect 包下的一個介面,該介面代表所有型別的公共高階介面,Class 是 Type 介面的實現類。Type 包括原始型別、引數化型別、陣列型別、型別變數和基本型別等。