Java反射的API在JavaSE1.7的時候已經基本完善,但是本文編寫的時候使用的是Oracle JDK11,因為JDK11對于sun包下的源碼也上傳了,可以直接通過IDE檢視對應的源碼和進行Debug。
本文主要介紹反射中一個比較難的問題-泛型。
泛型是在2004年JavaSE 5.0(JDK1.5)版本中添加到Java程式設計語言中的泛型程式設計工具。泛型的設計是為了應用在Java的類型系統,提供"用類型或者方法操作各種類型的對象進而提供編譯期的類型安全功能(原文:a type or method to operate on objects of various types while providing compile-time type safety)"。但是在2016年的一些研究表明,泛型并不是在所有的情況下都能保證編譯期的類型安全,例如切面(Aspect)程式設計的編譯期類型安全并沒有完全實作。
泛型的一個最大的優點就是:提供編譯期的類型安全。舉個很簡單的例子,在引入泛型之前,<code>ArrayList</code>内部隻維護了一個Object數組引用,這種做法有兩個問題:
從數組清單擷取一個元素的時候必須進行類型的強轉。
向數組清單中可以添加任何類型的對象,導緻無法得知數組清單中存放了什麼類型的元素。
引入泛型之後,我們可以通過類型參數明确定義<code>ArrayList</code>:
下面先列舉出Java中泛型的一些事實:
Java虛拟機中不存在泛型,隻有普通的類和方法,但是位元組碼中存放着泛型相關的資訊。
所有的類型參數都使用它們的限定類型替換。
橋方法(Bridge Method)由編譯器合成,用于保持多态(Java虛拟機利用方法的參數類型、方法名稱和方法傳回值類型确定一個方法)。
為了保持類型的安全性,必要時需要進行類型的強制轉換。
類型擦除(或者更多時候喜歡稱為"泛型擦除")的具體表現是:無論何時定義一個泛型類型,都自動提供一個相應的原始類型(Raw Type,這裡的原始類型并不是指int、boolean等基本資料類型),原始類型的類名稱就是帶有泛型參數的類删去泛型參數後的類型名稱,而原始類型會擦除(Erased)類型變量,并且把它們替換為限定類型(如果沒有指定限定類型,則擦除為Object類型),舉個例子<code>Pair<T></code>帶有泛型參數的類型如下:
擦除類型後的<code>Pair<T></code>的原始類型為:
舉個更複雜的例子,如果泛型參數類型是有上限的,變量會擦除為上限的類型:
類型擦除後的<code>Interval<T extends Comparable & Serializable></code>原始類型:
像上面這種多個泛型上限的類型,應該盡量把辨別接口上限類型放在邊界清單的尾部,這樣做可以提高效率。
在JDK1.5之前,也就是在泛型出現之前,所有的類型包括基本資料類型(int、byte等)、包裝類型、其他自定義的類型等等都可以使用類檔案(.class)位元組碼對應的<code>java.lang.Class</code>描述,也就是<code>java.lang.Class</code>類的一個具體執行個體對象就可以代表任意一個指定類型的原始類型。這裡把泛型出現之前的所有類型暫時稱為"曆史原始類型"。
在JDK1.5之後,資料類型得到了擴充,出曆史原始類型擴充了四種泛型類型:參數化類型(ParameterizedType)、類型變量類型(TypeVariable)、限定符類型(WildcardType)、泛型數組類型(GenericArrayType)。曆史原始類型和新擴充的泛型類型都應該統一成各自的位元組碼檔案類型對象,也就應該把泛型類型歸并進去<code>java.lang.Class</code>中。但是由于JDK已經疊代了很多版本,泛型并不屬于目前Java中的基本成分,如果JVM中引入真正的泛型類型,那麼必須涉及到JVM指令集和位元組碼檔案的修改(這個修改肯定不是小的修改,因為JDK當時已經疊代了很多年,而類型是程式設計語言的十分基礎的特性,引入泛型從項目功能疊代角度看可能需要整個JVM項目做回歸測試),這個功能的代價十分巨大,是以Java沒有在Java虛拟機層面引入泛型。
Java為了使用泛型,于是使用了類型擦除的機制引入了"泛型的使用",并沒有真正意義上引入和實作泛型。Java中的泛型實作的是編譯期的類型安全,也就是泛型的類型安全檢查是在編譯期由編譯器(常見的是javac)實作的,這樣就能夠確定資料基于類型上的安全性并且避免了強制類型轉換的麻煩(實際上,強制類型轉換是由編譯器完成了,隻是不需要人為去完成而已)。一旦編譯完成,所有的泛型類型都會被擦除,如果沒有指定上限,就會擦除為Object類型,否則擦除為上限類型。
既然Java虛拟機中不存在泛型,那麼為什麼可以從JDK中的一些類庫擷取泛型資訊?這是因為類檔案(.class)或者說位元組碼檔案本身存儲了泛型的資訊,相關類庫(可以是JDK的類庫,也可以是第三方的類庫)讀取泛型資訊的時候可以從位元組碼檔案中提取,例如比較常用的位元組碼操作類庫ASM就可以讀取位元組碼中的資訊甚至改造位元組碼動态生成類。例如前面提到的<code>Interval<T extends Comparable & Serializable></code>類,使用<code>javap -c -v</code>指令檢視其反編譯得到的位元組碼資訊,可以看到其簽名如下:
這裡的簽名資訊實際上是儲存在常量池中的,關于位元組碼檔案的解析将來會出一個系列文章詳細展開。
前文提到了在JDK1.5中引入了四種新的泛型類型<code>java.lang.reflect.ParameterizedType</code>、<code>java.lang.reflect.TypeVariable</code>、<code>java.lang.reflect.WildcardType</code>、<code>java.lang.reflect.GenericArrayType</code>,包括原來存在的<code>java.lang.Class</code>,一共存在五種類型。為了程式的擴充性,引入了<code>java.lang.reflect.Type</code>類作為這五種類型的公共父接口,這樣子就可以使用<code>java.lang.reflect.Type</code>類型參數去接收以上五種子類型的實參或者傳回值,由此從邏輯上統一了泛型相關的類型和原始存在的<code>java.lang.Class</code>描述的類型。Type體系如下:

注意:
ParameterizedType、TypeVariable、WildcardType、GenericArrayType都是接口,它們位于<code>java.lang.reflect</code>包中。
ParameterizedTypeImpl、TypeVariableImpl、WildcardTypeImpl、GenericArrayTypeImpl是四種泛型類型的實作,位于<code>sun.reflect.generics.reflectiveObjects</code>包中。
Type體系雖然看似很美好解決了泛型相關的類型和原始存在的<code>java.lang.Class</code>描述的類型的統一問題,但是引入了新的問題:如果一個方法傳回值為<code>java.lang.reflect.Type</code>類型,或者一個方法的入參類型為<code>java.lang.reflect.Type</code>類型,這兩種情況下,可能需要對<code>java.lang.reflect.Type</code>類型的對象做子類型判斷,因為它的子類型有可能是上面提到的五種類型中的其中一種,這一點提高了編碼的複雜性。
ParameterizedType,parameterized type,也就是參數化類型,注釋裡面說到<code>ParameterizedType</code>表示一個參數化類型,例如<code>Collection<String></code>,實際上隻要帶有參數化(泛型)标簽<code><ClassName></code>的參數或者屬性,都屬于ParameterizedType。例如下面的類型都是ParameterizedType:
而像下面的忽略泛型參數或者基本資料類型和基本資料類型的包裝類都不是ParameterizedType:
<code>java.lang.reflect.ParameterizedType</code>接口繼承自<code>java.lang.reflect.Type</code>接口,實作類是<code>sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl</code>,其實,必要的時候,我們也可以自行實作ParameterizedType,像一些Json解析工具都是自行實作ParameterizedType的。ParameterizedType接口的方法如下:
Type[] getActualTypeArguments():傳回這個ParameterizedType類型的參數的實際類型Type數組,Type數組裡面的元素有可能是Class、ParameterizedType、TypeVariable、GenericArrayType或者WildcardType之一。值得注意的是,無論泛型符号<code><></code>中有幾層<code><></code>嵌套,這個方法僅僅脫去最外層的<code><></code>,之後剩下的内容就作為這個方法的傳回值。
Type getRawType():傳回的是目前這個ParameterizedType的原始類型,從ParameterizedTypeImpl的源碼看來,原始類型rawType一定是一個<code>Class<?></code>執行個體。舉個例子,<code>List<Person></code>通過<code>getRawType()</code>擷取到的Type執行個體實際上是<code>Class<?></code>執行個體,和List.class等價。
Type getOwnerType():擷取原始類型所屬的類型,從ParameterizedTypeImpl的源碼看來,就是調用了原始類型rawType的<code>getDeclaringClass()</code>方法,而像rawType為<code>List<T></code>、<code>Map<T></code>這些類型的getOwnerType()實際上就是調用List.class.getDeclaringClass(),Map.class.getDeclaringClass(),傳回值都是null。
舉個關于ParameterizedType的簡單使用例子:
輸出結果:
TypeVariable,type variable,也就是類型變量,它是各種類型變量的公共父接口,它主要用來表示帶有上界的泛型參數的資訊,它和ParameterizedType不同的地方是,ParameterizedType表示的參數的最外層一定是已知具體類型的(如<code>List<String></code>),而TypeVariable面向的是K、V、E等這些泛型參數字面量的表示。常見的TypeVariable的表示形式是<code><T extends KnownType-1 & KnownType-2></code>。TypeVariable接口源碼如下:
Type[] getBounds():獲得該類型變量的上限(上邊界),若無顯式定義(extends),預設為Object,類型變量的上限可能不止一個,因為可以用&符号限定多個(這其中有且隻能有一個為類或抽象類,且必須放在extends後的第一個,即若有多個上邊界,則第一個&之後的必為接口)。
D getGenericDeclaration:獲得聲明(定義)這個類型變量的類型及名稱,會使用泛型的參數字面量表示,如<code>public void club.throwable.Main.query(java.util.List<club.throwable.Person>)</code>。
String getName():擷取泛型參數的字面量名稱,即K、V、E之類名稱。
AnnotatedType[] getAnnotatedBounds():Jdk1.8新增的方法,用于獲得注解類型的上限,若未明确聲明上邊界則預設為長度為0的數組。
舉個關于TypeVariable的簡單使用例子:
WildcardType用于表示通配符(?)類型的表達式的泛型參數,例如<code><? extends Number></code>等。根據WildcardType注釋提示:現階段通配符表達式僅僅接受一個上邊界或者下邊界,這個和定義類型變量時候可以指定多個上邊界是不一樣。但是為了保持擴充性,這裡傳回值類型寫成了數組形式。實際上現在傳回的數組的大小就是1。WildcardType接口源碼如下:
Type[] getUpperBounds():擷取泛型通配符的上限類型Type數組,實際上目前該數組隻有一個元素,也就是說隻能有一個上限類型。
Type[] getLowerBounds():擷取泛型通配符的下限類型Type數組,實際上目前該數組隻有一個元素,也就是說隻能有一個下限類型。
舉個關于WildcardType的簡單使用例子:
這裡注意的是<code>List<? extends Number> list</code>這個參數整體來看是ParameterizedType類型,剝掉第一次List之後的<code>? extends Number</code>是WildcardType類型。
GenericArrayType,generic array type,也就是泛型數組,也就是元素類型為泛型類型的數組實作了該接口。它要求元素的類型是ParameterizedType或TypeVariable(實際中發現元素是GenericArrayType也是允許的)。舉個例子:
GenericArrayType接口的源碼如下:
Type getGenericComponentType():擷取泛型數組中元素的類型。注意無論從左向右有幾個<code>[]</code>并列,這個方法僅僅脫去最右邊的<code>[]</code>之後剩下的内容就作為這個方法的傳回值。
舉個關于GenericArrayType的簡單使用例子:
這裡分析一下:
<code>String[] strings</code>:數組是Class類型。
<code>List<String> ls</code>:清單是ParameterizedType類型。
<code>List<String>[] lsa</code>:數組是GenericArrayType類型,調用getGenericComponentType後傳回的類型是<code>java.util.List<java.lang.String></code>,也就是數組元素是ParameterizedType類型。
<code>T[] ts</code>:s數組是GenericArrayType類型,調用getGenericComponentType後傳回的類型是T,也就是數組元素是TypeVariable類型。
<code>List<T>[] tla</code>:數組是GenericArrayType類型,調用getGenericComponentType後傳回的類型是<code>java.util.List<T></code>,也就是數組元素是ParameterizedType類型。
<code>T[][] tts</code>:數組是GenericArrayType類型,調用getGenericComponentType後傳回的類型T[],也就是數組元素是GenericArrayType類型。
使用Java泛型的時候需要考慮一些限制,這些限制大多數是由泛型類型擦除引起的。
1、不能用基本類型執行個體化類型參數,也就是8種基本類型不能作為泛型參數,例如<code>Pair<int></code>是非法的,會導緻編譯錯誤,而<code>Pair<Integer></code>是合法的。
2、運作時的類型查詢隻能适用于原始類型(非參數化類型)。
3、不能建立參數化類型的數組,例如<code>Pair<String>[] arr = new Pair<String>[10]</code>是非法的。
4、不能執行個體化類型變量或者類型變量數組,例如<code>T t = new T()</code>或者<code>T[] arr = new T[10]</code>都是非法的。
5、Varargs警告,這是因為第4點原因導緻的,一般會發生在泛型類型變量作為可變參數的情況,例如<code>public static <T> addAll(Collection<T> con,T ... ts)</code>,第二個參數實際上就是泛型類型變量數組,但是這種情況是合法的,不過會受到編譯器的警告,可以通過<code>@SuppressWarnings("unchecked")</code>注解或者<code>@SafeVarargs</code>注解标注該方法以消除警告。
6、不能在靜态域或者方法中引用類型變量,例如<code>private static T singleInstance;</code>這樣是非法的。
7、不能抛出或者抛出或者捕獲泛型類型變量,但是如果在異正常範中使用泛型類型變量則是允許的,舉兩個例子仔細品味一下:
8、通過使用<code>@SuppressWarnings("unchecked")</code>注解可以消除Java類型系統的部分基本限制,一般使用在強制轉換原始類型為泛型類型(隻是在編譯層面告知編譯器)的情況,如:
其實還有泛型的繼承規則和通配符規則(可以看下前面介紹的Type的子類型)等等,這裡不詳細展開。
在Java泛型限制中,無法執行個體化參數化類型數組,例如<code>Pair<Integer>[] table = new Pair<Integer>[10];</code>是非法的。根本原因在于泛型類型的擦除和數組會記錄元素類型的特性。舉個例子,假設可以執行個體化參數化類型數組:
上面的參數化類型數組在泛型擦除之後,數組執行個體table的類型為<code>Pair[]</code>,數組元素類型為<code>Pair</code>,可以強轉為<code>Object[]</code>類型數組:
基于泛型擦除,數組objArray可以任意指派<code>Pair<AnyType></code>的泛型化執行個體,例如:
這樣子能夠通過數組存儲元素的檢查,後續操作數組元素随時會出現ClassCastException。基于以上的原因,Java從編譯層面直接拒絕建立參數化類型數組。
另外,類型變量數組的執行個體化也是非法的,如<code>T[] tt = new T[10];</code>,這是因為類型變量僅僅是編譯期的字面量,其實和Java的類型體系是不相關的。
但是要注意一點:參數化類型數組和類型變量數組可以作為方法入參變量或者類的成員變量。例如下面的做法是合法的:
最後一點,可以檢視前一篇文章,其實可以使用反射建立泛型數組。
泛型中支援無限定通配符<code><?></code>,使用無限定通配符類型的執行個體有以下限制:
所有的Getter方法隻能傳回Object類型的值。
所有的Setter方法隻能指派null,其他類型的值的設定都是非法的。
無限定通配符類型可以看做原始類型的一個影子類型,它屏蔽了除了null之外的設值操作,所有擷取值的方法隻能傳回Object類型結果,這種特性使得通過無限定通配符類型進行一些簡單的操作變得十分友善,例如:
如果反射用得比較熟的話,<code>java.lang.Class</code>也有類似的用法:
先說明一下什麼是橋方法,看下面的代碼:
父類<code>Supper<T></code>在泛型擦除後原始類型是:
子類<code>Sub</code>雖然實作了父類<code>Supper</code>,但是它隻實作了<code>void method(Integer value)</code>而沒有實作父類中的<code>void method(Object t)</code>,這個時候,編譯期編譯器會為子類<code>Sub</code>建立此方法,也就是子類<code>Sub</code>會變成這樣:
如果你直接這樣編寫一個子類<code>Sub</code>是會編譯報錯,而上面這裡編譯器生成的<code>void method(Object value)</code>方法就是橋方法。可以用反射驗證一下:
橋方法的定義比較模糊,是以這裡隻考慮它出現的情況,不做盲目的定義。不單隻是子類實作帶有泛型參數的父類會産生橋方法,還有一種比較常見的情況是在方法覆寫的時候指定一個更加"嚴格的"傳回值類型的時候,也會産生橋方法,例如:
這是因為:
編譯的時候Java的方法簽名是方法名稱加上方法參數類型清單,也就是方法名和參數類型清單确定一個方法的簽名(這樣就可以很好了解方法重載,還有Java中的參數都是形參,是以參數名稱沒有實質意義,隻有參數類型才是有意義的)。
Java虛拟機定義一個方法的簽名是由方法名稱、方法傳回值類型和方法參數類型清單組成,是以JVM認為傳回值類型不同,而方法名稱和參數類型清單一緻的方法是不相同的方法。
仔細看,其實兩種情況都是由于繼承才導緻橋方法出現。
這裡列舉一下JDK中筆者所知的操作泛型的相關API(可以會有遺漏),這些API主要和反射操作相關:
<code>java.lang.Class</code>中的相關方法:
方法
功能
Type[] getGenericInterfaces()
傳回類執行個體的接口的泛型類型
Type getGenericSuperclass()
傳回類執行個體的父類的泛型類型
<code>java.lang.reflect.Constructor</code>中的相關方法:
Type[] getGenericExceptionTypes()
傳回構造器的異常的泛型類型
Type[] getGenericParameterTypes()
傳回構造器的方法參數的泛型類型
<code>java.lang.reflect.Method</code>中的相關方法:
傳回方法的異常的泛型類型
傳回方法參數的泛型類型
Type getGenericReturnType()
傳回方法傳回值的泛型類型
<code>java.lang.reflect.Field</code>中的相關方法:
Type getGenericType()
傳回屬性的泛型類型
如果在使用上面的方法得到的傳回值和期望的傳回值不相同,請加深對泛型類型擦除的認識。
參考資料:
個人認為,泛型其實是JDK疊代過程中妥協和相容曆史的産物,它是一種沒有實作的泛型,當然,提供編譯期類型安全這一點可以讓開發者避免類型轉換出現人為錯誤,也就是說:Java中的泛型使得程式或者代碼的可讀性和安全性提高,這是它的最大優勢。
《Java核心技術卷I-基礎知識》
維基百科-Generics in Java
Throwable's Blog
(本文完 e-a-20181205)
技術公衆号(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):