2.3 Java類型信息詳解
運行時類型信息(RTTI)使得我們可以在程序運行時發現和使用類型信息,其工作原理是Class對象中包含了與類有關的信息。
2.3.1 Class對象
每一個類有一個Class對象,編譯期生成,保存在同名的.class文件中。這些Class對象包含了這個類型的父類、接口、構造函數、方法、屬性等詳細信息,這些class文件在程序運行時會被ClassLoader加載到JVM中,在JVM中就表現為一個Class對象,JVM使用該Class對象創建該類的所有常規對象。
需要注意的是,Class對象和其他對象一樣,我們可以獲取并操作它的引用,反射就是基于這一點進行的。
instanceof和isInstance
首先這兩者是一個語義:引用的實際類型(而非靜態類型)是否和指定類型相匹配。兩者的區別在于:
- 前者是Java的操作符,而該語法要求instanceof運算符的右操作數是一個引用類型名,即編譯期常量
- 后者是Java標準庫里的一個方法,其被調用對象等價于instanceof運算符的右操作數的意義,但可以是運行時的java.lang.Class對象,而不要求是編譯時常量,比instanceOf運算符更靈活。
我們可以使用Class.isInstance()方法可以寫出這樣的代碼:
public static boolean areTypesCompatible(Object expected, Object obj) {
return Objects.requireNonNull(expected)
.getClass()
.isInstance(obj);
}
來檢查obj所引用的對象的實際類型是否為expected所引用的對象的實際類型的子類。顯然我們不能用instanceof來實現這個功能:
return obj instanceof expected.getClass(); // doesn't compile
這里簡要說明一下其原理:首先,每個類T都有一個T.class對象,其是Class類對象,而這個T.class對象中含有類T的信息:類型名、成員、父類、接口、加載器等。具體判斷一般是如下步驟(假設要檢查的對象引用是obj,目標的類型對象是T):
- obj如果為null,則返回false;否則設S為obj的類型對象,剩下的問題就是檢查S是否為T的子類型。
- 如果S == T,則返回true。
- 接下來分為3種情況,S是數組類型、接口類型或者類類型。之所以要分情況是因為instanceof要做的是“子類型檢查”,而Java語言的類型系統里數組類型、接口類型與普通類類型三者的子類型規定都不一樣,必須分開來討論。其中需要注意的是:對接口類型的instanceof就直接遍歷S里記錄的它所實現的接口,看有沒有跟T一致的;而對類類型的instanceof則是遍歷S的super鏈(繼承鏈)一直到Object,看有沒有跟T一致的,遍歷類的super鏈意味著這個算法的性能會受類的繼承深度的影響。
2.3.2 Java類的加載、鏈接和初始化
有關類的加載、連接和初始化,在之后的JVM詳解中將會詳細介紹,這里就不再詳述。
創建自己的類加載器
在Java應用開發過程中,可能會需要創建應用自己的類加載器。典型的場景包括實現特定的Java字節碼查找方式,對字節代碼進行加密/解密以及實現同名Java類的隔離等。創建自己的類加載器并不是一件復雜的事情,只需要繼承自java.lang.ClassLoader類并覆寫對應的方法即可。ClassLoader中提供的方法有不少,下面介紹幾個創建類加載器時需要考慮的:
- defineClass():這個方法用來完成從Java字節碼的字節數組到java.lang.Class的轉換。這個方法是不能被覆寫的,一般是native實現。
- findLoadedClass():這個方法用來根據名稱查找已經加載過的Java類,一個類加載器不會重復加載同一個類。
- findClass():這個方法用來根據名稱查找并加載Java類,當我們在實現自己的類加載器時應該覆寫該方法來實現自己的類加載邏輯。
- loadClass():這個方法用來根據名稱加載Java類。
- resolveClass():這個方法用來鏈接一個Java類。
類加載器的代理模式默認使用的是父類優先的策略,這個策略的實現是封裝在loadClass中的,如果希望修改此策略,就要覆寫loadClass方法。下面的代碼給出了自定義的類加載器的常見實現模式:
public class MyClassLoader extends ClassLoader {
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = null; //查找或生成Java類的字節代碼
return defineClass(name, b, 0, b.length);
}
}
2.3.3 反射(運行時的類信息)
通過反射可以獲取程序在運行時刻的內部結構,而知道了一個Java類的內部結構之后,就可以與它進行交互,包括創建新的對象和調用對象中的方法等。
基本用法
反射的第一個主要作用是獲取程序在運行時刻的內部結構,這對于程序的檢查工具和調試器來說,是非常實用的功能,可以很簡單地遍歷出來一個Java類的內部結構,包括其中的構造方法、聲明的域和定義的方法等。
反射的另外一個作用是在運行時刻對一個Java對象進行操作。這些操作包括動態創建一個Java類的對象,獲取某個域的值以及調用某個方法。在Java源代碼中編寫的對類和對象的操作,都可以在運行時刻通過反射來實現。
只要有了其Class類的對象,就可以通過其中的方法來獲取到該類中的構造方法、域和方法。對應的方法分別是getConstructor、getField和getMethod。這三個方法還有相應的getDeclaredXXX版本,區別在于getDeclaredXXX版本的方法只會獲取該類自身所聲明的元素,而不會考慮繼承下來的。Constructor、Field和Method這三個類分別表示類中的構造方法、域和方法。這些類中的方法可以獲取到所對應結構的元數據。考慮下面一個簡單的例子:
public class MyClass {
public int count;
public MyClass(int start) {
count = start;
}
public void increase(int step) {
count = count + step;
}
}
...
public void testRefelct() {
try {
Class<?> clazz = Class.forName("com.app.MyClass");
//獲取構造方法
Constructor constructor = clazz.getConstructor(int.class);
//創建對象
MyClass myClassReflect = constructor.newInstance(10);
//獲取方法
Method method = MyClass.class.getMethod("increase", int.class);
//調用方法
method.invoke(myClassReflect, 5);
//獲取域
Field field = MyClass.class.getField("count");
//獲取域的值
System.out.println("Reflect -> " + field.getInt(myClassReflect));
} catch (Exception e) {
e.printStackTrace();
}
}
需要注意的是,數組對象比較特殊,Array類提供了一系列的靜態方法用來創建數組和對數組中的元素進行訪問和操作。
Object array = Array.newInstance(String.class, 10); //等價于 new String[10]
Array.set(array, 0, "Hello"); //等價于array[0] = "Hello"
Array.set(array, 1, "World"); //等價于array[1] = "World"
System.out.println(Array.get(array, 0)); //等價于array[0]
使用反射可以繞過Java默認的訪問控制檢查,比如可以直接獲取到對象的私有域的值或是調用私有方法。只需要在獲取到Constructor、Field和Method類的對象之后,調用setAccessible方法并設為true即可。有了這種機制,就可以很方便的在運行時刻獲取到程序的內部狀態。
需要注意的是:通過反射創建出來的對象,一般要繼續使用反射去做字段訪問或者方法調用;或者是如果這個反射創建出來的對象實現了已知接口的話,可以cast成已知接口的引用然后來用。
處理泛型
Java引入了泛型后,反射也做了相應的修改,以提供對泛型的支持。由于類型擦除機制的存在,泛型類中的類型參數等信息,在運行時刻是不存在的,JVM看到的都是原始類型。對此,Java5對Java類文件的格式做了修訂,添加了Signature屬性,用來包含不在JVM類型系統中的類型信息。在運行時刻,JVM會讀取Signature屬性的內容并提供給反射API來使用。比如在代碼中聲明了一個域是List<String>類型的,雖然在運行時刻其類型會變成原始類型List,但是仍然可以通過反射來獲取到所用的實際的類型參數
Field field = Pair.class.getDeclaredField("myList"); //myList的類型是List
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) type;
Type[] actualTypes = paramType.getActualTypeArguments();
for (Type aType : actualTypes) {
if (aType instanceof Class) {
Class clz = (Class) aType;
System.out.println(clz.getName()); //輸出java.lang.String
}
}
}
動態代理
在代理模式里,代理對象和被代理對象一般實現相同的接口,調用者與代理對象進行交互,代理的存在對于調用者來說是透明的。代理對象則可以封裝一些內部的處理邏輯,如訪問控制、遠程通信、日志、緩存等。比如一個對象訪問代理就可以在普通的訪問機制之上添加緩存的支持。傳統的代理模式的實現,需要在源代碼中添加一些附加的類。這些類一般是手寫或是通過工具來自動生成。Java5引入了動態代理機制,允許開發人員在運行時刻動態的創建出代理類及其對象。在運行時刻,可以動態創建出一個實現了多個接口的代理類。
每個代理類的對象都會關聯一個表示內部處理邏輯的InvocationHandler接口的實現。當使用者調用了代理對象所代理的接口中的方法的時候,這個調用的信息會被傳遞給InvocationHandler的invoke方法。在invoke方法的參數中可以獲取到代理對象、方法對應的Method對象和調用的實際參數。invoke方法的返回值被返回給使用者。這種做法實際上相當于對方法調用進行了攔截。
下面的代碼用來代理一個實現了List接口的對象,所實現的功能也非常簡單,那就是禁止使用List接口中的add方法。如果在getList中傳入一個實現List接口的對象,那么返回的實際就是一個代理對象,嘗試在該對象上調用add方法就會拋出來異常。
public List getList(final List list) {
return (List) Proxy.newProxyInstance(DummyProxy.class.getClassLoader(), new Class[]{List.class}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("add".equals(method.getName())) {
throw new UnsupportedOperationException();
} else {
return method.invoke(list, args);
}
}
});
}
這里的實際流程是,當代理對象的add方法被調用的時候,InvocationHandler中的invoke方法會被調用。參數method就包含了調用的基本信息,如果調用的是add方法,就會拋出異常;如果調用的是其它方法的話,則執行原來的邏輯。
使用案例
反射的存在,為Java語言添加了一定程度上的動態性,可以實現某些動態語言中的功能。反射實際上定義了一種相對于編譯時刻而言更加松散的契約。如果被調用的Java對象中并不包含某個方法,而在調用者代碼中進行引用的話,在編譯時刻就會出現錯誤。而反射則可以把這樣的檢查推遲到運行時刻來完成,這一點在框架開發中尤其重要。通過把Java中的字節代碼增強、類加載器和反射結合起來,可以處理一些對靈活性要求很高的場景。比如在有些情況下,可能會需要從遠端加載一個Java類來執行,客戶端Java程序可以通過網絡從服務器端下載Java類來執行,從而可以實現自動更新的機制。一般的做法是下載了類字節代碼(或者源碼)之后,通過自定義類加載器加載出Class類的對象,再通過反射就可以創建出實例了。