《徐徐道來話Java》(2):泛型和數組,以及Java是如何實現泛型的


 數組和泛型容器有什么區別

  要區分數組和泛型容器的功能,這里先要理解三個概念:協變性(covariance)、逆變性(contravariance)和無關性(invariant)。

  若類A是類B的子類,則記作A ≦ B。設有變換f(),若:

       當A ≦ B時,有f(A)≦ f(B),則稱變換f()具有協變性;

    當A ≦ B時,有f(B)≦ f(A),則稱變換f()具有逆變性;

    如果以上兩者皆不成立,則稱變換f()具有無關性。

  在Java中,數組具有協變性,而泛型具有無關性,示例代碼如下:

Object[] array = new String[10];
//編譯錯誤 ArrayList
<Object> list=new ArrayList<String>();

  這兩句代碼,數組正常編譯通過,而泛型拋出了編譯期錯誤,應用之前提出的概念對代碼進行分析,可知:

1、String ≦ Object 2、數組的變換可以表達為f(A)=A[],通過之前的示例,可以得出下推論:   f(String) = String[] 以及 f(Object) = Object[]; 4、通過代碼驗證,String[] ≦ Object[] 是成立的,由此可見,數組具有協變性。

  又可知:

  5、ArrayList泛型的變換可以表達為 f(A)= ArrayList<A>,得出推論:     f(String) = ArrayList<String> 以及 f(Object) = ArrayList<Object>;   6、通過代碼驗證,ArrayList<String> ≦ ArrayList<Object>不成立,由此可見,泛型具備無關性

  最終得出結論,數組具備協變性,而泛型具備無關性

  所以,為了讓泛型具備協變性和逆變性,Java引入了有界泛型(參見3.1.2小節內容)概念。

  除了協變性的不同,數組還是具象化的,而泛型不是

  什么是具象化(reified,也可以稱之為具體化,物化)?

  在Java語言規范》里,明確的規定了具象化類型的定義:

完全在運行時可用的類型被稱為具象化類型(refiable type),會做這種區分是因為有些類型會在編譯過程中被擦除,並不是所有的類型都在運行時可用。

它包括:

1、非泛型類聲明,接口類型聲明;

2、所有泛型參數類型為無界通配符(僅用‘?’修飾)的泛型參數類;

3、原始類型;

4、基本數據類型;

5、其元素類型為具象化類型的數組;

6、嵌套類(內部類,匿名內部類等,比如java.util.HashMap.Entry),並且嵌套過程中的每一個類都是具象化的。

  不論是在編譯時還是運行時,數組都能確切的知道自己的所屬的類型。但是泛型在編譯時會丟失部分類型信息,在運行時,它又會被當作Object處理。

  這里要涉及到類型擦除的相關知識,會在后面詳細解釋。在當前,只需要知道,Java的泛型最后都被當作上界(此概念會在后面說明)處理了。

  引申:數組具備協變性,是Java的一個缺陷,因為極少有地方需要用到數組的協變性,甚至,使用數組的協變會引起不易檢查的運行時異常,參見下面代碼:

 

Object[] array = new String[10];

array[0] = 1;

 

 

  很明顯,這會在運行期拋出異常:java.lang.ArrayStoreException。

  鑒於有如此多的不同,在Java里,數組和泛型是不能混合使用的。參見下面代碼:

List<String>[] genericListArray = new ArrayList<String>[10];

T[] genericArray = new T[];

  它們都會在編譯期拋出Cannot create a generic array錯誤。這是因為,數組要求類型是具象化(refied)的,而泛型恰好不是。

  換言之,數組必須清楚的知道自己內部元素的類型,並且會一直保存這個類型信息,在添加的時候元素的時候,該信息會用於做類型檢查,而泛型的類型不確定。所以,在編譯器層面就杜絕了這個問題。這在《Java語言規范》里有明確的說明:

If the element type of an array were not reifiable,the virtual machine could not perform the store check described in the preceding paragraph. This is why creation of arrays of non-reifiable types is forbidden. One may declare variables of array types whose element type is not reifiable, but any attempt to assign them a value will give rise to an unchecked warning .

如果數組的元素類型不是具象化的,虛擬機將無法應用在前面章節里描述過的存儲檢查。這就是為什么禁止創建(實例化)非具象化的數組。你可以定義(聲明)一個元素類型是非具象化的數組類型,但任何師徒給它分配一個值的操作,都會產生一個unchecked warning。

存儲檢查:這里涉及到Array的基本原理,可以自行參閱《Java語言規范》或者參考5.1.1ArrayList相關章節

 

  這不得不說,又是Java在泛型設計上的一點缺陷,為什么Java的泛型設計會有這么多缺陷呢?難道真的是Java語言不夠好嗎?這些內容將在3.3節泛型歷史中解答。

泛型使用建議

  泛型在Java開發和設計中占據了重要的地位,如果正確高效的使用泛型尤為重要。下面通過介紹兩條使用泛型時的建議,來加深對泛型的理解:

  1、泛型類型只能是類類型,不能是基本數據類型,如果要使用基本數據類型作為泛型,應當使用其對應的包裝類。比如,如果期望在List中存放整形變量,因為int是基本類型,所以不能使用List<int>,應該使用int的包裝類Integer,所以正確的使用方法為List<Integer>。

  當然,泛型不支持基本數據類型,試圖使用基本數據類型作為泛型的時候必須轉化為包裝類這點,是Java泛型設計之初的缺陷。

  2、使用到集合的時候,盡量的使用泛型集合來替代非泛型集合。一般來說,軟件的開發期和維護期時間占比,也是符合二八定律的,維護期的時長能超出開發期數倍。使用了泛型的集合至少,在IDE工具上,是類型確定的,可以提高代碼的可讀性,並在編譯期就避免一些嚴重的BUG。

  3、不要使用常見類名(尤其是String這種屬於java.lang的)作為泛型名,會造成編譯器無法區分開類和泛型,並且不會拋出異常。

泛型擦除

 

  在學習泛型擦除之前,明確一個概念:Java的泛型不存在於運行時。這也是為什么有人說Java沒有真正的泛型。

  泛型擦除(類型擦除),它是指在編譯器處理帶泛型定義的類\接口\方法時,會在字節碼指令集里抹去全部泛型類型信息,被擦除后泛型,在字節碼里只保留泛型的原始類型(raw type)。

  原始類型,是指抹去泛型信息后的類型,在Java中,它必須是一個引用類型(非基本數據類型),一般而言,它對應的是泛型的定義上界。

  舉例:<T>中的T對應的原始泛型是Object,<T extends String>對應的原始類型就是String。

 泛型信息會在編譯時擦除

 

  如何證明泛型會被擦除呢?這里提供了一段測試代碼:

class TypeErasureSample<T> { public T v1; public T v2; public String v3; } /** * 泛型擦除示例 */

public class Generic3_2 { public static void main(String[] args) throws Exception { TypeErasureSample<String> type = new TypeErasureSample<String>(); type.v1 = "String value"; // 反射設置v2的值為整型數
 Field v2 = TypeErasureSample.class.getDeclaredField("v2"); v2.set(type, 1); for (Field f : TypeErasureSample.class.getDeclaredFields()) { System.out.println(f.getName() + ":" + f.getType()); } /* * 此處會拋出java.lang.ClassCastException: java.lang.Integer cannot be cast * to java.lang.String */ System.out.println(type.v2); } }

 

  程序運行結果為:

v1:class java.lang.Object

v2:class java.lang.Object

v3:class java.lang.String

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

at capter3.generic.Generic3_2.main(Generic3_2.java:29)

  v1和v2的類型被指定為泛型T,但是通過反射發現,它們實質上還是Object,而v3原本定義的就是String,和前兩項一比對,證明反射本身並無錯誤。

  代碼在輸出type.v2的過程中拋出了類型轉換異常,這說明了兩件事:

  1、為v2設置整型數已經成功(可以自行寫一段反射來驗證);

  2、編譯器在構建字節碼的時候,一定做了類似於(String)type.v2的強行轉換,關於這一點,可以通過反編譯驗證(反編譯工具為jd-gui),結果如下所示:

 

public class Generic3_2 { public static void main(String[] args) throws Exception { TypeErasureSample type = new TypeErasureSample(); type.v1 = "String value"; Field v2 = TypeErasureSample.class.getDeclaredField("v2"); v2.set(type, Integer.valueOf(1)); for (Field f : TypeErasureSample.class.getDeclaredFields()) { System.out.println(f.getName() + ":" + f.getType()); } System.out.println((String)type.v2); } }

 

  可以看到,如果編譯器認為type.v2有被申明為String的必要的時候,都會加上(String)強行轉換。可以進行測試:

  Object o = type.v2;

  String s = type .v2;

  后者會拋出類型轉換異常,而前者是正常執行的。由此,可以得出結論,編譯器會在構建字節碼的時候,抹去一些泛型信息。

 

編譯器保留的泛型信息有哪些?

 

  上一節中介紹了編譯器會擦除全部泛型信息,那么是不是所有的泛型信息都會在編譯的過程中消失呢,答案是否定的字節碼里指令集之外的地方,會保留部分泛型信息。下面的泛型在編譯階段是會被保留的:

  1、泛型接口、類、方法定義上的所有泛型;

  2、成員變量聲明處的泛型。

  參考下面的代碼:

/** * 定義了泛型參數的接口 */

interface GI<T> { } /** * 定義了泛型參數並實現了泛型接口的類 */

class GC<T> implements GI<T> { // 兩種使用了泛型的成員變量
 T m1; ArrayList<T> m2 = new ArrayList<T>(); /** * 定義了泛型參數的方法,並在返回值、參數和異常拋出位置使用了該泛型 */

<K extends Exception> ArrayList<K> method(K p) throws K { // 在方法體中使用了泛型
 K k = p; ArrayList<K> list = new ArrayList<K>(); list.add(k); return list; } }

 

  代碼涵蓋了泛型的各種聲明和使用情況。接下來使用反編譯工具看看結果,可以注意到,接口、類、方法定義的位置,大部分泛型信息依然存在,字段中使用到泛型作為聲明的位置,泛型同樣存在,而在所有在局部代碼快對泛型做引用的位置,泛型內容消失了:

 

 

abstract interface GI<T>{

}

 

class GC<T>  implements GI<T>{

  T m1;

  ArrayList<T> m2 = new ArrayList();

 

  <K extends Exception> ArrayList<K> method(K p) throws Exception{

    Exception k = p;

    ArrayList list = new ArrayList();

    list.add(k);

    return list;

  }

}

 

  可以注意到,在之前沒有提及的位置,比如GC.m2成員變量的實例化位置,method方法體里的泛型信息全部被擦除。

  為什么Java會這么設計?這也很好理解:

    1、如果不保留泛型定義,那么除非擁有源碼,不然無法使用泛型。

    2、即使保留了泛型定義,定義位置的泛型信息並未初始化,也就是說,泛型參數沒有綁定為特定的某個類,對使用者不具備意義。而且,泛型信息在運行時也會被處理為上界,對使用並不會有影響。

  相信注意細節的讀者已經發現了,之前提及的“會被保留泛型信息的位置”里,“異常拋出位置”的K被替換為了Exception,這不正說明它被擦除了?

  事實上,如果通過反射來獲取泛型信息的時候(方法將在下一小節詳細講解),會發現,依然可以得到異常的泛型信息。得出結論,作為拋出異常的泛型參數,沒有消失

  這是為什么呢?

  既然反編譯工具沒有記錄下泛型信息,只能說明某些反編譯工具沒有解析二進制文件里的某些信息。這些信息是什么呢?這里要引入的一個概念,方法簽名(Method Signatrue)。

  下面列出的是上一個例子的部分字節碼內容(也就是class文件反編譯的原始內容):

  // Method descriptor #31 (Ljava/lang/Exception;)Ljava/util/ArrayList;

  // Signature: <K:Ljava/lang/Exception;>(TK;)Ljava/util/ArrayList<TK;>;^TK;

  // Stack: 1, Locals: 2

  java.util.ArrayList method(java.lang.Exception p) throws java.lang.Exception;

    0  aconst_null

    1  areturn

      Line numbers:

        [pc: 0, line: 40]

      Local variable table:

        [pc: 0, pc: 2] local: this index: 0 type: capter3.generic.GC

        [pc: 0, pc: 2] local: p index: 1 type: java.lang.Exception

      Local variable type table:

        [pc: 0, pc: 2] local: this index: 0 type: capter3.generic.GC<T>

        [pc: 0, pc: 2] local: p index: 1 type: K

 

  這段內容不長,也無需細看,如果稍微觀察下,可以注意到第四行開始就是方法的定義部分,包括返回值ArrayList,參數Exception,拋出的異常Exception,注意到沒有?它們,統統不帶泛型信息,而在更早之前的位置(1-3行)可以看到三段注釋,這就是之前所說的方法簽名了

  方法簽名是方法定義的一部分,它規定了方法的參數列表和返回值等信息。下面來詳細解釋下各個部分的概念。

第一行:

  // Method descriptor #31 (Ljava/lang/Exception;)Ljava/util/ArrayList;

  Method descriptor是標志方法簽名的開始。

  #ID是該方法的id號,在同一個方法體內不會重復。

  (參數列表)表示方法有一個Exception類型的形參,類名前的L是引用類型的標記;基礎數據類型的標記是對應類型的首字母大寫,比如int對應I。數組的標記是在原始標記前加上符號[,比如double[]對應[D,String[]對應[Ljava/lang/String。

  最后的位置是返回值,比如Ljava/util/ArrayList;表示方法的返回值是ArrayList。

 

第二行:

  // Signature: <K:Ljava/lang/Exception;>(TK;)Ljava/util/ArrayList<TK;>;^TK;

  Signature是簽名的意思,標識開始的關鍵字,這一行對應的就是泛型了。

  <泛型參數名:上界>這部分對應的是方法的泛型描述。

  (參數列表)和第一行的大體意思一致,但是多了泛型的定義,在字節碼中,泛型會用其上界來替代(擦除),如果沒有定義上界,則默認為Object,真正的泛型的定義就出現在本行的這個位置。用T前綴來表示泛型,比如泛型K就對應TK;。

  緊跟着參數列表的是返回值。該返回值描述和第一行的返回值描述一致,不過,同樣多了泛型的描述,也是用T前綴來表達,比如返回值是java.util.ArrayList,這里就變為Ljava/util/ArrayList<TK;>;。

  ^泛型異常,用於描述用泛型表達的異常,如果異常不是泛型,則該部分描述不會生成。比如throws K就會被描述為^TK;。

 

第三行:

   // Stack: 1, Locals: 2

  Stack,表達的是調用棧(call stack),用於描述在調用棧上最多有多少個對象。為什么會有個這個棧呢?是因為“局部變量”這個概念對於虛擬機來說,是不存在的,所以在某個方法被調用前,需要把該方法要用到的變量都加載到一個全局調用棧內。方法被虛擬機喚起的時候,只需要按順序傳入變量類型,然后自動從調用棧里按需取得變量。

  每次操作執行完成后,棧被清空,所以,棧深等同為變量最多的操作的變量數。

  Locals,用於描述使用到的本地變量,讀者可能會疑惑,該方法里明明只用到了一個形參K,為什么會有兩個變量呢?這是因為java默認給方法注冊了一個this,作為本地變量。

  懂得了字節碼的真相,也就懂得了Java泛型的實現原理。

  Java的方法泛型沒有記錄在方法體內部,而是在方法簽名內做了實現。同樣,可以在字節碼里找到類\接口簽名,類字段(成員變量)簽名等等。

 

  換言之,Java的泛型是由編譯時擦除和簽名來實現的。

  Java這樣的設計,是為了兼容性的考慮,低版本的字節碼和高版本基本上只有簽名上的不一樣,不影響功能本體,所以,可以不做任何改動就在高版本的虛擬機里運行。

 

 反射獲取泛型信息

 

  上一節中提到了如下的一些泛型信息不會被擦除:

  1、泛型接口、類、方法定義上處的所有泛型

  2、成員變量聲明處的泛型

  可以得出推論,這些泛型信息應當能夠被反射獲取。

  對這些能被反射獲取的內容,按照泛型的分類來進行討論:

  1、泛型接口和泛型類。它們對應的反射對象都是java.reflect.Class,該類提供了三個方法:

 

public Type getGenericSuperclass(){...}

public Type[] getGenericInterfaces() {...}

public TypeVariable<Class<T>>[] getTypeParameters() {...}

 

 

  分別對應:獲取超類的完整類型,獲取接口的完整類型,以及獲取自身的類型變量。

  java.lang.reflect.Type是一個空接口,在使用標准JDK的情況下,一般來說,泛型的實現類是:sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl。

  它提供了獲取原始類型和泛型類型的方法。

  java.lang.reflect.TypeVariable是Type的子接口,它提供的方法就比Type要詳細一些,這些多出來的方法包括:

 

 Type[] getBound(),獲取上界;

    D getGenericDeclaration(),獲取泛型定義;

    String getName(),獲取泛型參數名,也就是<T>中的T。

 

  2、聲明為泛型的字段。它對應的反射對象是java.reflect.Field,提供了一個方法:

public Type getGenericType() {...}

  該方法的使用方式和上文一致。

 

  3、泛型方法。對應的反射對象是java.reflect.Method,提供了三個方法:

public Type getGenericReturnType() {...}

public Type getGenericParameterTypes() {...}

public Type getGenericExceptionTypes() {...}

 

  分別對應返回值泛型,參數泛型和異常泛型。

  注意!雖然這里可以獲取到泛型的定義,但不論是哪一種方式,其獲取到的泛型,都不會是具體的某一個類。給定一個泛型的定義<T>,能獲取到的只有T這個關鍵字。

  這是因為,Java目前的泛型實現已經在原理上(泛型擦除)堵死了“反射獲取泛型的確定類型”的可能性。

 

  泛型的原理和基本概念到這里已經講解得差不多了,后面會介紹一下Java泛型的歷史,以說明為什么Java的泛型為什么有這么多的“缺陷”。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM