Java泛型的反射

1. 類型擦除

瞭解編譯器揹着我們做了什麼很重要。Java中的泛型,在編譯後會被擦除類型參數。

如果用instanceof來查詢對象的類型,只能查到對應的原始類型(raw type)。

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();
        List<String> stringList = new ArrayList<>();
        Class c1 = integerList.getClass();
        Class c2 = stringList.getClass();
        System.out.println(c1 == c2);   // true
        System.out.println(c1);         // class java.util.ArrayList
        System.out.println(c2);         // class java.util.ArrayList
        System.out.println(integerList instanceof ArrayList);   // true
    }

注意,像下面這樣寫會報錯:

integerList instanceof ArrayList<Integer>

也就是說,我們用 instanceof 來判斷一個對象的類型的時候,只能到raw type這個層面,更進一步的類型參數是無法判斷的。

也正因爲編譯器會擦除類型信息,如下的方法重載會在編譯期報錯,因爲類型擦除後,這兩個方法的參數列表是一樣的。


void method(List<String> list){}
void method(List<Integer> list){}

下面摘一段《Core Java》對Java泛型的總結

虛擬機中沒有泛型,只有普通的類和方法

所以的類型參數都用它們的限定類型替換

橋方法被合成用來保持多態

爲保證類型安全性,必要時插入強制類型轉換

題外話:編譯器在包裝類型和基本類型的轉換中也會插手,自動裝箱、自動拆箱指的就是編譯器會插入強制類型轉換的代碼。注意這是編譯期做的,不是運行期。

2. 對泛型的反射

Java中的泛型和繼承結合起來,就會涉及到協變、逆變的問題。同樣的,由於類型擦除,在用反射處理泛型類型時,處理方法又有區別。JDK1.5爲此提供了一個新的接口Type。

雖然有類型擦除,但也不是所有的地方都會被擦除,具體參見R大的這篇博客http://rednaxelafx.iteye.com/blog/586212

下面通過代碼說明在使用反射獲取泛型的綁定類型時的一種場景,以及如何在該場景下獲取綁定的具體類型。

在繼續下面的內容前,應該對Java中的ParameterizedType、TypeVariable、Class這些類型有基本的瞭解。


首先定義一個泛型類,以及一個繼承了泛型類的具體類。

public class User<T> {
    private String name;
    private int age;
    private T data;
}

class VipUser extends User<String> {}

需要注意的是,這裏User類是一個泛型,但是VipUser繼承的時候,將類型參數綁定到了String類型。

我們先用反射來處理User類。對於非泛型字段,一切正常;但是對於泛型字段,我們只能拿到類型Object。

        Class<?> c = User.class;
        // 非泛型字段 name
        Field nameField = c.getDeclaredField("name");
        // false
        System.out.println(nameField.getGenericType() instanceof ParameterizedType);
        // false
        System.out.println(nameField.getGenericType() instanceof TypeVariable);
        // true
        System.out.println(nameField.getGenericType() instanceof Class);
        // class java.lang.String
        System.out.println(nameField.getGenericType());     
        
        // 泛型字段 data
        Field dataField = c.getDeclaredField("data");
        // true
        System.out.println(dataField.getGenericType() instanceof TypeVariable); 
        // T
        System.out.println(dataField.getGenericType());
        // class java.lang.Object
        System.out.println(dataField.getType());
         

接下來我們看看對VipUser類的處理:

        Class<?> c = VipUser.class;
        Class<?> parent = c.getSuperclass();
        Field dataField = parent.getDeclaredField("data");
        // class java.lang.Object (getType方法只能拿到Object,拿不到具體類型)
        System.out.println(dataField.getType());
        // T (仍然拿不到具體類型)
        System.out.println(dataField.getGenericType());
        // true
        System.out.println(dataField.getGenericType() instanceof TypeVariable);
        // 通過MyBatis的TypeParameterResolver類來獲得字段類型
        // class java.lang.String
        System.out.println(TypeParameterResolver.resolveFieldType(dataField, c));

不管是通過getGenericType方法還是getType方法,我們都拿不到定義VipUser類時綁定的String類型,但是通過MyBatis的TypeParameterResolver類,我們得到了data字段的正確類型-String。有興趣的可以參考一下MyBatis的源碼,看看是怎麼做的。

下面的代碼參考MyBatis的TypeParameterResolver類的源碼,針對上面的例子,說明如何拿到泛型綁定的具體類型。


        Class<?> c = VipUser.class;
        // 注意是調用getGenericSuperclass方法
        ParameterizedType parentAsType = (ParameterizedType) c.getGenericSuperclass();
        Class<?> parentAsClass = (Class<?>) parentAsType.getRawType();
        Field dataField = parentAsClass.getDeclaredField("data");
        // T
        Type genericType = dataField.getGenericType();
        // 獲取泛型的類型參數
        TypeVariable[] typeVar = parentAsClass.getTypeParameters();
        for (int i = 0; i < typeVar.length; i++) {
            if (genericType.equals(typeVar[i])) {
                // class java.lang.String
                System.out.println(parentAsType.getActualTypeArguments()[i]);
                break;
            }
        }

關於反射和泛型碰到一起時還有很多場景,以上給出的只是其中一種。具體的可以參考MyBatis的TypeParameterResolver類。


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