計算機程式的思維邏輯 (84) – 反射

swiftma發表於2019-03-01

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (84) – 反射

上節介紹完了併發,從本節開始,我們來探討Java中的一些動態特性,包括反射、類載入器、註解和動態代理等。利用這些特性,可以以優雅的方式實現一些靈活和通用的功能,經常用於各種框架、庫和系統程式中,比如:

  • 63節介紹的實用序列化庫Jackson,利用反射和註解實現了通用的序列化/反序列化機制
  • 有多種庫如Spring MVC, Jersey用於處理Web請求,利用反射和註解,能方便的將使用者的請求引數和內容轉換為Java物件,將Java物件轉變為響應內容
  • 有多種庫如Spring, Guice利用這些特性實現了物件管理容器,方便程式設計師管理物件的生命週期以及其中複雜的依賴關係
  • 應用伺服器比如Tomcat利用類載入器實現不同應用之間的隔離、JSP技術也利用類載入器實現修改程式碼不用重啟就能生效的特性
  • 面向方面的程式設計(AOP – Aspect Oriented Programming)將程式設計中通用的關注點比如日誌記錄、安全檢查等與業務的主體邏輯相分離,減少冗餘程式碼,提高程式的可維護性,AOP需要依賴上面的這些特性來實現

本節先來看反射機制。

在一般運算元據的時候,我們都是知道並且依賴於資料的型別的,比如:

  • 根據型別使用new建立物件
  • 根據型別定義變數,型別可能是基本型別、類、介面或陣列
  • 將特定型別的物件傳遞給方法
  • 根據型別訪問物件的屬性,呼叫物件的方法

編譯器也是根據型別,進行程式碼的檢查編譯。

反射不一樣,它是在執行時,而非編譯時,動態獲取型別的資訊,比如介面資訊、成員資訊、方法資訊、構造方法資訊等,根據這些動態獲取到的資訊建立物件、訪問/修改成員、呼叫方法等。這麼說比較抽象,下面我們會具體來說明,反射的入口是名稱為”Class”的類,我們來看下。

“Class”類

獲取Class物件

我們在17節介紹過類和繼承的基本實現原理,我們提到,每個已載入的類在記憶體都有一份類資訊,每個物件都有指向它所屬類資訊的引用。Java中,類資訊對應的類就是java.lang.Class,注意不是小寫的class,class是定義類的關鍵字,所有類的根父類Object有一個方法,可以獲取物件的Class物件:

public final native Class<?> getClass()
複製程式碼

Class是一個泛型類,有一個型別引數,getClass()並不知道具體的型別,所以返回Class<?>。

獲取Class物件不一定需要例項物件,如果在寫程式時就知道類名,可以使用<類名>.class獲取Class物件,比如:

Class<Date> cls = Date.class;
複製程式碼

介面也有Class物件,且這種方式對於介面也是適用的,比如:

Class<Comparable> cls = Comparable.class;
複製程式碼

基本型別沒有getClass方法,但也都有對應的Class物件,型別引數為對應的包裝型別,比如:

Class<Integer> intCls = int.class;
Class<Byte> byteCls = byte.class;
Class<Character> charCls = char.class;
Class<Double> doubleCls = double.class;
複製程式碼

void作為特殊的返回型別,也有對應的Class:

Class<Void> voidCls = void.class;
複製程式碼

對於陣列,每種型別都有對應陣列型別的Class物件,每個維度都有一個,即一維陣列有一個,二維陣列有一個不同的,比如:

String[] strArr = new String[10];
int[][] twoDimArr = new int[3][2];
int[] oneDimArr = new int[10];
Class<? extends String[]> strArrCls = strArr.getClass();
Class<? extends int[][]> twoDimArrCls = twoDimArr.getClass();
Class<? extends int[]> oneDimArrCls = oneDimArr.getClass();
複製程式碼

列舉型別也有對應的Class,比如:

enum Size {
    SMALL, MEDIUM, BIG
}

Class<Size> cls = Size.class;
複製程式碼

Class有一個靜態方法forName,可以根據類名直接載入Class,獲取Class物件,比如:

try {
    Class<?> cls = Class.forName("java.util.HashMap");
    System.out.println(cls.getName());
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}
複製程式碼

注意forName可能丟擲異常ClassNotFoundException。

有了Class物件後,我們就可以瞭解到關於型別的很多資訊,並基於這些資訊採取一些行動,Class的方法很多,大部分比較簡單直接,容易理解,下面,我們分為若干組,進行簡要介紹。

名稱資訊

Class有如下方法,可以獲取與名稱有關的資訊:

public String getName()
public String getSimpleName()
public String getCanonicalName()
public Package getPackage()
複製程式碼

getSimpleName不帶包資訊,getName返回的是Java內部使用的真正的名字,getCanonicalName返回的名字更為友好,getPackage返回的是包資訊,它們的不同可以看如下表格:

計算機程式的思維邏輯 (84) – 反射

需要說明的是陣列型別的getName返回值,它使用字首[表示陣列,有幾個[表示是幾維陣列,陣列的型別用一個字元表示,I表示int,L表示類或介面,其他型別與字元的對應關係為: boolean(Z), byte(B), char(C), double(D), float(F), long(J), short(S),對於引用型別的陣列,注意最後有一個分號”;”。

欄位(例項和靜態變數)資訊

類中定義的靜態和例項變數都被稱為欄位,用類Field表示,位於包java.util.reflect下,後文涉及到的反射相關的類都位於該包下,Class有四個獲取欄位資訊的方法:

//返回所有的public欄位,包括其父類的,如果沒有欄位,返回空陣列
public Field[] getFields()
//返回本類宣告的所有欄位,包括非public的,但不包括父類的
public Field[] getDeclaredFields()
//返回本類或父類中指定名稱的public欄位,找不到丟擲異常NoSuchFieldException
public Field getField(String name)
//返回本類中宣告的指定名稱的欄位,找不到丟擲異常NoSuchFieldException
public Field getDeclaredField(String name)
複製程式碼

Field也有很多方法,可以獲取欄位的資訊,也可以通過Field訪問和操作指定物件中該欄位的值,基本方法有:

//獲取欄位的名稱
public String getName()
//判斷當前程式是否有該欄位的訪問許可權
public boolean isAccessible()
//flag設為true表示忽略Java的訪問檢查機制,以允許讀寫非public的欄位
public void setAccessible(boolean flag)
//獲取指定物件obj中該欄位的值
public Object get(Object obj)
//將指定物件obj中該欄位的值設為value
public void set(Object obj, Object value)
複製程式碼

在get/set方法中,對於靜態變數,obj被忽略,可以為null,如果欄位值為基本型別,get/set會自動在基本型別與對應的包裝型別間進行轉換,對於private欄位,直接呼叫get/set會丟擲非法訪問異常IllegalAccessException,應該先呼叫setAccessible(true)以關閉Java的檢查機制。

看段簡單的示例程式碼:

List<String> obj = Arrays.asList(new String[]{"老馬","程式設計"});
Class<?> cls = obj.getClass();
for(Field f : cls.getDeclaredFields()){
    f.setAccessible(true);
    System.out.println(f.getName()+" - "+f.get(obj));
}
複製程式碼

程式碼比較簡單,就不贅述了。我們在ThreadLocal一節介紹過利用反射來清空ThreadLocal,這裡重複下其程式碼,含義就比較清楚了:

protected void beforeExecute(Thread t, Runnable r) {
    try {
        //使用反射清空所有ThreadLocal
        Field f = t.getClass().getDeclaredField("threadLocals");
        f.setAccessible(true);
        f.set(t, null);
    } catch (Exception e) {
        e.printStackTrace();
    }
    super.beforeExecute(t, r);
}
複製程式碼

除了以上方法,Field還有很多別的方法,比如:

//返回欄位的修飾符
public int getModifiers()
//返回欄位的型別
public Class<?> getType()
//以基本型別操作欄位
public void setBoolean(Object obj, boolean z)
public boolean getBoolean(Object obj)
public void setDouble(Object obj, double d)
public double getDouble(Object obj)
//查詢欄位的註解資訊
public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
public Annotation[] getDeclaredAnnotations()
複製程式碼

getModifiers返回的是一個int,可以通過Modifier類的靜態方法進行解讀,比如,假定Student類有如下欄位:

public static final int MAX_NAME_LEN = 255;
複製程式碼

可以這樣檢視該欄位的修飾符:

Field f = Student.class.getField("MAX_NAME_LEN");
int mod = f.getModifiers();
System.out.println(Modifier.toString(mod));
System.out.println("isPublic: " + Modifier.isPublic(mod));
System.out.println("isStatic: " + Modifier.isStatic(mod));
System.out.println("isFinal: " + Modifier.isFinal(mod));
System.out.println("isVolatile: " + Modifier.isVolatile(mod));
複製程式碼

輸出為:

public static final
isPublic: true
isStatic: true
isFinal: true
isVolatile: false
複製程式碼

關於註解,我們下節再詳細介紹。

方法資訊

類中定義的靜態和例項方法都被稱為方法,用類Method表示,Class有四個獲取方法資訊的方法:

//返回所有的public方法,包括其父類的,如果沒有方法,返回空陣列
public Method[] getMethods()
//返回本類宣告的所有方法,包括非public的,但不包括父類的
public Method[] getDeclaredMethods()
//返回本類或父類中指定名稱和引數型別的public方法,找不到丟擲異常NoSuchMethodException
public Method getMethod(String name, Class<?>... parameterTypes)
//返回本類中宣告的指定名稱和引數型別的方法,找不到丟擲異常NoSuchMethodException
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
複製程式碼

Method也有很多方法,可以獲取方法的資訊,也可以通過Method呼叫物件的方法,基本方法有:

//獲取方法的名稱
public String getName()
//flag設為true表示忽略Java的訪問檢查機制,以允許呼叫非public的方法
public void setAccessible(boolean flag)
//在指定物件obj上呼叫Method代表的方法,傳遞的引數列表為args
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
複製程式碼

對invoke方法,如果Method為靜態方法,obj被忽略,可以為null,args可以為null,也可以為一個空的陣列,方法呼叫的返回值被包裝為Object返回,如果實際方法呼叫丟擲異常,異常被包裝為InvocationTargetException重新丟擲,可以通過getCause方法得到原異常。

看段簡單的示例程式碼:

Class<?> cls = Integer.class;
try {
    Method method = cls.getMethod("parseInt", new Class[]{String.class});
    System.out.println(method.invoke(null, "123"));
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
}
複製程式碼

Method還有很多方法,可以獲取方法的修飾符、引數、返回值、註解等資訊,比如:

//獲取方法的修飾符,返回值可通過Modifier類進行解讀
public int getModifiers()
//獲取方法的引數型別
public Class<?>[] getParameterTypes()
//獲取方法的返回值型別
public Class<?> getReturnType()
//獲取方法宣告丟擲的異常型別
public Class<?>[] getExceptionTypes()
//獲取註解資訊
public Annotation[] getDeclaredAnnotations()
public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
//獲取方法引數的註解資訊
public Annotation[][] getParameterAnnotations() 
複製程式碼

建立物件和構造方法

Class有一個方法,可以用它來建立物件:

public T newInstance() throws InstantiationException, IllegalAccessException
複製程式碼

它會呼叫類的預設構造方法(即無參public構造方法),如果類沒有該構造方法,會丟擲異常InstantiationException。看個簡單示例:

Map<String,Integer> map = HashMap.class.newInstance();
map.put("hello", 123);
複製程式碼

很多利用反射的庫和框架都預設假定類有無參public構造方法,所以當類利用這些庫和框架時要記住提供一個。

newInstance只能使用預設構造方法,Class還有一些方法,可以獲取所有的構造方法:

//獲取所有的public構造方法,返回值可能為長度為0的空陣列
public Constructor<?>[] getConstructors()
//獲取所有的構造方法,包括非public的
public Constructor<?>[] getDeclaredConstructors()
//獲取指定引數型別的public構造方法,沒找到丟擲異常NoSuchMethodException
public Constructor<T> getConstructor(Class<?>... parameterTypes)
//獲取指定引數型別的構造方法,包括非public的,沒找到丟擲異常NoSuchMethodException
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 
複製程式碼

類Constructor表示構造方法,通過它可以建立物件,方法為:

public T newInstance(Object ... initargs) throws InstantiationException, 
     IllegalAccessException, IllegalArgumentException, InvocationTargetException
複製程式碼

比如:

Constructor<StringBuilder> contructor= StringBuilder.class
                    .getConstructor(new Class[]{int.class});
StringBuilder sb = contructor.newInstance(100);
複製程式碼

除了建立物件,Constructor還有很多方法,可以獲取關於構造方法的很多資訊,比如:

//獲取引數的型別資訊
public Class<?>[] getParameterTypes()
//構造方法的修飾符,返回值可通過Modifier類進行解讀
public int getModifiers()
//構造方法的註解資訊
public Annotation[] getDeclaredAnnotations()
public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
//構造方法中引數的註解資訊
public Annotation[][] getParameterAnnotations() 
複製程式碼

型別檢查和轉換

我們在16節介紹過instanceof關鍵字,它可以用來判斷變數指向的實際物件型別,instanceof後面的型別是在程式碼中確定的,如果要檢查的型別是動態的,可以使用Class類的如下方法:

public native boolean isInstance(Object obj)
複製程式碼

也就是說,如下程式碼:

if(list instanceof ArrayList){
    System.out.println("array list");
}
複製程式碼

和下面程式碼的輸出是相同的:

Class cls = Class.forName("java.util.ArrayList");
if(cls.isInstance(list)){
    System.out.println("array list");
}
複製程式碼

除了判斷型別,在程式中也往往需要進行強制型別轉換,比如:

List list = ..
if(list instanceof ArrayList){
    ArrayList arrList = (ArrayList)list;
}
複製程式碼

在這段程式碼中,強制轉換到的型別是在寫程式碼時就知道的,如果是動態的,可以使用Class的如下方法:

public T cast(Object obj)
複製程式碼

比如:

public static <T> T toType(Object obj, Class<T> cls){
    return cls.cast(obj);
}
複製程式碼

isInstance/cast描述的都是物件和類之間的關係,Class還有一個方法,可以判斷Class之間的關係:

// 檢查引數型別cls能否賦給當前Class型別的變數
public native boolean isAssignableFrom(Class<?> cls);
複製程式碼

比如,如下表示式的結果都為true:

Object.class.isAssignableFrom(String.class)
String.class.isAssignableFrom(String.class)
List.class.isAssignableFrom(ArrayList.class)
複製程式碼

Class的型別資訊

Class代表的型別既可以是普通的類、也可以是內部類,還可以是基本型別、陣列等,對於一個給定的Class物件,它到底是什麼型別呢?可以通過以下方法進行檢查:

//是否是陣列
public native boolean isArray();  
//是否是基本型別
public native boolean isPrimitive();
//是否是介面
public native boolean isInterface();
//是否是列舉
public boolean isEnum()
//是否是註解
public boolean isAnnotation()
//是否是匿名內部類
public boolean isAnonymousClass()
//是否是成員類
public boolean isMemberClass()
//是否是本地類
public boolean isLocalClass() 
複製程式碼

需要說明下匿名內部類、成員類與本地類的區別,本地類是指在方法內部定義的非匿名內部類,比如,如下程式碼:

public static void localClass(){
    class MyLocal {
    }
    Runnable r = new Runnable() {
        @Override
        public void run(){
            
        }
    };
    System.out.println(MyLocal.class.isLocalClass());
    System.out.println(r.getClass().isLocalClass());
}
複製程式碼

MyLocal定義在localClass方法內部,就是一個本地類,r的物件所屬的類是一個匿名類,但不是本地類。

成員類也是內部類,定義在類內部、方法外部,它不是匿名類,也不是本地類。

類的宣告資訊

Class還有很多方法,可以獲取類的宣告資訊,如修飾符、父類、實現的介面、註解等,如下所示:

//獲取修飾符,返回值可通過Modifier類進行解讀
public native int getModifiers()
//獲取父類,如果為Object,父類為null
public native Class<? super T> getSuperclass()
//對於類,為自己宣告實現的所有介面,對於介面,為直接擴充套件的介面,不包括通過父類間接繼承來的
public native Class<?>[] getInterfaces();
//自己宣告的註解
public Annotation[] getDeclaredAnnotations()
//所有的註解,包括繼承得到的
public Annotation[] getAnnotations()
//獲取或檢查指定型別的註解,包括繼承得到的
public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
複製程式碼

內部類

關於內部類,Class有一些專門的方法,比如:

//獲取所有的public的內部類和介面,包括從父類繼承得到的
public Class<?>[] getClasses()
//獲取自己宣告的所有的內部類和介面
public Class<?>[] getDeclaredClasses()
//如果當前Class為內部類,獲取宣告該類的最外部的Class物件
public Class<?> getDeclaringClass()
//如果當前Class為內部類,獲取直接包含該類的類
public Class<?> getEnclosingClass()
//如果當前Class為本地類或匿名內部類,返回包含它的方法
public Method getEnclosingMethod()
複製程式碼

類的載入

Class有兩個靜態方法,可以根據類名載入類:

public static Class<?> forName(String className)
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
複製程式碼

ClassLoader表示類載入器,後面章節我們會進一步介紹,initialize表示載入後,是否執行類的初始化程式碼(如static語句塊)。第一個方法中沒有傳這些引數,相當於呼叫:

Class.forName(className, true, currentLoader)
複製程式碼

currentLoader表示載入當前類的ClassLoader。

這裡className與Class.getName的返回值是一致的,比如,對於String陣列:

String name = "[Ljava.lang.String;";
Class cls = Class.forName(name);
System.out.println(cls == String[].class);
複製程式碼

需要注意的是,基本型別不支援forName方法,也就是說,如下寫法:

Class.forName("int");
複製程式碼

會丟擲異常ClassNotFoundException,那如何根據原始型別的字串構造Class物件呢?可以對Class.forName進行一下包裝,比如:

public static Class<?> forName(String className) throws ClassNotFoundException{
    if("int".equals(className)){
        return int.class;
    }
    //其他基本型別...
    return Class.forName(className);
}
複製程式碼

反射與陣列

對於陣列型別,有一個專門的方法,可以獲取它的元素型別:

public native Class<?> getComponentType()
複製程式碼

比如:

String[] arr = new String[]{};
System.out.println(arr.getClass().getComponentType());
複製程式碼

輸出為:

class java.lang.String
複製程式碼

java.lang.reflect包中有一個針對陣列的專門的類Array(注意不是java.util中的Arrays),提供了對於陣列的一些反射支援,以便於統一處理多種型別的陣列,主要方法有:

//建立指定元素型別、指定長度的陣列,
public static Object newInstance(Class<?> componentType, int length)
//建立多維陣列
public static Object newInstance(Class<?> componentType, int... dimensions)
//獲取陣列array指定的索引位置index處的值
public static native Object get(Object array, int index)
//修改陣列array指定的索引位置index處的值為value
public static native void set(Object array, int index, Object value)
//返回陣列的長度
public static native int getLength(Object array)
複製程式碼

需要注意的是,在Array類中,陣列是用Object而非Object[]表示的,這是為什麼呢?這是為了方便處理多種型別的陣列,int[],String[]都不能與Object[]相互轉換,但可以與Object相互轉換,比如:

int[] intArr = (int[])Array.newInstance(int.class, 10);
String[] strArr = (String[])Array.newInstance(String.class, 10);
複製程式碼

除了以Object型別運算元組元素外,Array也支援以各種基本型別運算元組元素,如:

public static native double getDouble(Object array, int index)
public static native void setDouble(Object array, int index, double d)
public static native void setLong(Object array, int index, long l)
public static native long getLong(Object array, int index)
複製程式碼

反射與列舉

列舉型別也有一個專門方法,可以獲取所有的列舉常量:

public T[] getEnumConstants()
複製程式碼

應用示例

介紹了Class的這麼多方法,有什麼用呢?我們看個簡單的示例,利用反射實現一個簡單的通用序列化/反序列化類SimpleMapper,它提供兩個靜態方法:

public static String toString(Object obj)
public static Object fromString(String str)
複製程式碼

toString將物件obj轉換為字串,fromString將字串轉換為物件。為簡單起見,我們只支援最簡單的類,即有預設構造方法,成員型別只有基本型別、包裝類或String。另外,序列化的格式也很簡單,第一行為類的名稱,後面每行表示一個欄位,用字元`=`分隔,表示欄位名稱和字串形式的值。SimpleMapper可以這麼用:

public class SimpleMapperDemo {
    static class Student {
        String name;
        int age;
        Double score;

        public Student() {
        }

        public Student(String name, int age, Double score) {
            super();
            this.name = name;
            this.age = age;
            this.score = score;
        }

        @Override
        public String toString() {
            return "Student [name=" + name + ", age=" + age + ", score=" + score + "]";
        }
    }

    public static void main(String[] args) {
        Student zhangsan = new Student("張三", 18, 89d);
        String str = SimpleMapper.toString(zhangsan);
        Student zhangsan2 = (Student) SimpleMapper.fromString(str);
        System.out.println(zhangsan2);
    }
}
複製程式碼

程式碼先呼叫toString方法將物件轉換為了String,然後呼叫fromString方法將字串轉換為了Student,新物件的值與原物件是一樣的,輸出如下所示:

Student [name=張三, age=18, score=89.0]
複製程式碼

我們來看SimpleMapper的示例實現(主要用於演示原理,在生產中謹慎使用),toString的程式碼為:

public static String toString(Object obj) {
    try {
        Class<?> cls = obj.getClass();
        StringBuilder sb = new StringBuilder();
        sb.append(cls.getName() + "
");
        for (Field f : cls.getDeclaredFields()) {
            if (!f.isAccessible()) {
                f.setAccessible(true);
            }
            sb.append(f.getName() + "=" + f.get(obj).toString() + "
");
        }
        return sb.toString();
    } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}
複製程式碼

fromString的程式碼為:

public static Object fromString(String str) {
    try {
        String[] lines = str.split("
");
        if (lines.length < 1) {
            throw new IllegalArgumentException(str);
        }
        Class<?> cls = Class.forName(lines[0]);
        Object obj = cls.newInstance();
        if (lines.length > 1) {
            for (int i = 1; i < lines.length; i++) {
                String[] fv = lines[i].split("=");
                if (fv.length != 2) {
                    throw new IllegalArgumentException(lines[i]);
                }
                Field f = cls.getDeclaredField(fv[0]);
                if(!f.isAccessible()){
                    f.setAccessible(true);
                }
                setFieldValue(f, obj, fv[1]);
            }
        }
        return obj;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
複製程式碼

它呼叫了setFieldValue方法對欄位設定值,其程式碼為:

private static void setFieldValue(Field f, Object obj, String value) throws Exception {
    Class<?> type = f.getType();
    if (type == int.class) {
        f.setInt(obj, Integer.parseInt(value));
    } else if (type == byte.class) {
        f.setByte(obj, Byte.parseByte(value));
    } else if (type == short.class) {
        f.setShort(obj, Short.parseShort(value));
    } else if (type == long.class) {
        f.setLong(obj, Long.parseLong(value));
    } else if (type == float.class) {
        f.setFloat(obj, Float.parseFloat(value));
    } else if (type == double.class) {
        f.setDouble(obj, Double.parseDouble(value));
    } else if (type == char.class) {
        f.setChar(obj, value.charAt(0));
    } else if (type == boolean.class) {
        f.setBoolean(obj, Boolean.parseBoolean(value));
    } else if (type == String.class) {
        f.set(obj, value);
    } else {
        Constructor<?> ctor = type.getConstructor(new Class[] { String.class });
        f.set(obj, ctor.newInstance(value));
    }
}
複製程式碼

setFieldValue根據欄位的型別,將字串形式的值轉換為了對應型別的值,對於基本型別和String以外的型別,它假定該型別有一個以String型別為引數的構造方法。

反射與泛型

在介紹泛型的時候,我們提到,泛型引數在執行時會被擦除,這裡,我們需要補充一下,在類資訊Class中依然有關於泛型的一些資訊,可以通過反射得到,泛型涉及到一些更多的方法和類,上面的介紹中進行了忽略,這裡簡要補充下。

Class有如下方法,可以獲取類的泛型引數資訊:

public TypeVariable<Class<T>>[] getTypeParameters()
複製程式碼

Field有如下方法:

public Type getGenericType()
複製程式碼

Method有如下方法:

public Type getGenericReturnType()
public Type[] getGenericParameterTypes()
public Type[] getGenericExceptionTypes()
複製程式碼

Constructor有如下方法:

public Type[] getGenericParameterTypes() 
複製程式碼

Type是一個介面,Class實現了Type,Type的其他子介面還有:

  • TypeVariable:型別引數,可以有上界,比如:T extends Number
  • ParameterizedType:引數化的型別,有原始型別和具體的型別引數,比如:List
  • WildcardType:萬用字元型別,比如:?, ? extends Number, ? super Integer

我們看一個簡單的示例:

public class GenericDemo {
    static class GenericTest<U extends Comparable<U>, V> {
        U u;
        V v;
        List<String> list;

        public U test(List<? extends Number> numbers) {
            return null;
        }
    }

    public static void main(String[] args) throws Exception {
        Class<?> cls = GenericTest.class;
        // 類的型別引數
        for (TypeVariable t : cls.getTypeParameters()) {
            System.out.println(t.getName() + " extends " + Arrays.toString(t.getBounds()));
        }

        // 欄位 - 泛型型別
        Field fu = cls.getDeclaredField("u");
        System.out.println(fu.getGenericType());

        // 欄位 - 引數化的型別
        Field flist = cls.getDeclaredField("list");
        Type listType = flist.getGenericType();
        if (listType instanceof ParameterizedType) {
            ParameterizedType pType = (ParameterizedType) listType;
            System.out.println("raw type: " + pType.getRawType() + ",type arguments:"
                    + Arrays.toString(pType.getActualTypeArguments()));
        }

        // 方法的泛型引數
        Method m = cls.getMethod("test", new Class[] { List.class });
        for (Type t : m.getGenericParameterTypes()) {
            System.out.println(t);
        }
    }
}
複製程式碼

程式的輸出為:

U extends [java.lang.Comparable<U>]
V extends [class java.lang.Object]
U
raw type: interface java.util.List,type arguments:[class java.lang.String]
java.util.List<? extends java.lang.Number>
複製程式碼

程式碼比較簡單,我們就不贅述了。

慎用反射

反射雖然是靈活的,但一般情況下,並不是我們優先建議的,主要原因是:

  • 反射更容易出現執行時錯誤,使用顯式的類和介面,編譯器能幫我們做型別檢查,減少錯誤,但使用反射,型別是執行時才知道的,編譯器無能為力
  • 反射的效能要低一些,在訪問欄位、呼叫方法前,反射先要查詢對應的Field/Method,效能要慢一些

簡單的說,如果能用介面實現同樣的靈活性,就不要使用反射

小結

本節介紹了Java中反射相關的主要類和方法,通過入口類Class,可以訪問類的各種資訊,如欄位、方法、構造方法、父類、介面、泛型資訊等,也可以建立和操作物件,呼叫方法等,利用這些方法,可以編寫通用的、動態靈活的程式,本節演示了一個簡單的通用序列化/反序列化類SimpleMapper。反射雖然是靈活通用的,但它更容易出現執行時錯誤,所以,能用介面代替的時候,應該儘量使用介面。

本節介紹的很多類如Class/Field/Method/Constructor都可以有註解,註解到底是什麼呢?

(與其他章節一樣,本節所有程式碼位於 github.com/swiftma/pro…,位於包shuo.laoma.dynamic.c84下)


未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),從入門到高階,深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。

計算機程式的思維邏輯 (84) – 反射

相關文章