Java中的泛型不是語言內(nèi)在的機(jī)制,而是后來添加的特性,,這樣就帶來一個(gè)問題:非泛型代碼和泛型代碼的兼容性,。泛型是JDK1.5才添加到Java中的,那么之前的代碼全部都是非泛型的,,它們?nèi)绾芜\(yùn)行在JDK1.5及以后的VM上,?為了實(shí)現(xiàn)這種兼容性,Java泛型被局限在一個(gè)很狹窄的地方,,同時(shí)也讓它變得難以理解,,甚至可以說是Java語言中最難理解的語法。
擦除
為了實(shí)現(xiàn)與非泛型代碼的兼容,,Java語言的泛型采用擦除(Erasure)來實(shí)現(xiàn),,也就是泛型基本上由編譯器來實(shí)現(xiàn),由編譯器執(zhí)行類型檢查和類型推斷,,然后在生成字節(jié)碼之前將其清除掉,,虛擬機(jī)是不知道泛型存在的。這樣的話,,泛型和非泛型的代碼就可以混合運(yùn)行,,當(dāng)然了,也顯得相當(dāng)混亂,。
在使用泛型時(shí),,會有一個(gè)對應(yīng)的類型叫做原生類型(raw type),泛型類型會被擦除到原生類型,,如Generic<T>會被查處到Generic,,List<String>會被查處到List,由于擦除,,在虛擬機(jī)中無法獲得任何類型信息,,虛擬機(jī)只知道原生類型,。下面的代碼將展示Java泛型的真相-擦除:
- class Erasure<T> {
- private T t;
-
- public void set(T t) {
- this.t = t;
- }
-
- public T get() {
- return t;
- }
-
- public static void main(String[] args) {
- Erasure<String> eras = new Erasure<String>();
- eras.set("not real class type");
- String value = eras.get();
-
- }
- }
使用javap反編譯class文件,得到如下代碼:
- class com.think.generics.Erasure<T> {
- com.think.generics.Erasure();
- Code:
- 0: aload_0
- 1: invokespecial #12 // Method java/lang/Object."<init>":()V
- 4: return
-
- public void set(T);
- Code:
- 0: aload_0
- 1: aload_1
- 2: putfield #23 // Field t:Ljava/lang/Object;
- 5: return
-
- public T get();
- Code:
- 0: aload_0
- 1: getfield #23 // Field t:Ljava/lang/Object;
- 4: areturn
-
- public static void main(java.lang.String[]);
- Code:
- 0: new #1 // class com/think/generics/Erasure
- 3: dup
- 4: invokespecial #30 // Method "<init>":()V
- 7: astore_1
- 8: aload_1
- 9: ldc #31 // String not real class type
- 11: invokevirtual #33 // Method set:(Ljava/lang/Object;)V
- 14: aload_1
- 15: invokevirtual #35 // Method get:()Ljava/lang/Object;
- 18: checkcast #37 // class java/lang/String
- 21: astore_2
- 22: return
- }
從反編譯出來的字節(jié)碼可以看到,,泛型Erasure<T>被擦除到了Erasure,,其內(nèi)部的字段T被擦除到了Object,可以看到get和set方法中都是把t作為Object來使用的,。最值得關(guān)注的是,,反編譯代碼的倒數(shù)第三行,對應(yīng)到Java代碼就是String value = eras.get();編譯器執(zhí)行了類型轉(zhuǎn)換,。這就是Java泛型的本質(zhì):對傳遞進(jìn)來的值進(jìn)行額外的編譯期檢查,,并插入對傳遞出去的值的轉(zhuǎn)型。這樣的泛型真的是泛型嗎,?
即便我們可以說,,Java中的泛型確實(shí)不是真正的泛型,但是它帶來的好處還是顯而易見的,,它使得Java的類型安全前進(jìn)了一大步,,原本需要程序員顯式控制的類型轉(zhuǎn)換,現(xiàn)在改由編譯器來實(shí)現(xiàn),,只要你按照泛型的規(guī)范去編寫代碼,,總會得到安全的保障。在這里,,我們不得不思考一個(gè)問題,,理解Java泛型,那么其核心目的是什么,?我個(gè)人認(rèn)為,,Java泛型的核心目的在于安全性,尤其是在理解泛型通配符時(shí),,一切奇怪的規(guī)則,,歸根結(jié)底都是處于安全的目的。
類型信息的丟失
由于擦除的原因,,在泛型代碼內(nèi)部,無法獲得任何有關(guān)泛型參數(shù)類型的信息,。在運(yùn)行時(shí),,虛擬機(jī)無法獲得確切的類型信息,一切以來確切類型信息的工作都無法完成,,比如instanceof操作,,和new表達(dá)式,
- class Erasure<T> {
- public void f() {
- if(arg instanceof T) //Error
- T ins = new T();//Error
- T[] array = new T[10];//error
- }
- }
那么在需要具體的類型信息時(shí),,我們就要記住Class對象來實(shí)現(xiàn)了,,凡是在運(yùn)行時(shí)需要類型信息的地方,,都使用Class對象來進(jìn)行操作,比如:
- class Erasure<T> {
- private Class<T> clazz;
- Erasure(Class<T> kind) {
- clazz = kind;
- }
- public void f() {
- if(clazz.isInstance(arg)) {}
- T t = clazz.newInstance();//必須要有無參構(gòu)造方法
- }
- }
泛型類中的數(shù)組
數(shù)組是Java語言中的內(nèi)建特性,,將泛型與數(shù)組結(jié)合就會有一些難以理解的問題,。首先Java中的數(shù)組是協(xié)變的,Integer是Number的子類,,所以Integer[]也是Number[]的子類,,凡是使用Number[]的地方,都可以使用Integer[]來代替,,而泛型是不協(xié)變的,,比如List<String>不是List<Object>的子類,在通配符中,,會詳細(xì)討論這些情況,。
由于無法獲得確切的類型信息,我們怎么樣創(chuàng)建泛型數(shù)組呢,?在Java中,,所有類的父類都是Object,所以可以創(chuàng)造Object類型的數(shù)組來代替泛型數(shù)組:
- public class Array<T> {
- private int size = 0;
- private Object[] array;
-
- public Array(int size) {
- this.size = size;
- array = new Object[size];
- }
- //編譯器會保證插入進(jìn)來的是正確類型
- public void put(int index,T item) {
- array[index] = item;
- }
-
- //顯式的類型轉(zhuǎn)換
- public T get(int index) {
- return (T)array[index];
- }
-
- public T[] rep() {
- return (T[])array;
- }
-
- private static class Father {}
- private static class Son extends Father {}
-
- public static void main(String[] args) {
- Array<String> instance = new Array<String>(10);
- String[] array = instance.rep();//異常
-
- }
- }
在上面的代碼中,,get()和put()都可以正確的運(yùn)行,,編譯器會保證類型的正確性。但是當(dāng)rep()返回時(shí)賦給String[]類型的數(shù)組,,則會拋出ClassCastException異常,,拋出這樣的異常是在意料之中的。在Java中,,數(shù)組其實(shí)是一個(gè)對象,,每一個(gè)類型的數(shù)組都后一個(gè)對應(yīng)的類,這個(gè)類是虛擬機(jī)生成,,比如上面的代碼中,,我們定義了Object數(shù)組,在運(yùn)行時(shí)會生成一個(gè)名為"[Ljava.lang.Object"的類,,它代表Object的一維數(shù)組,;同樣的,定義String[]數(shù)組,,其對應(yīng)的類是"[Ljava.lang.String",。從類名就可以看出,這些代表數(shù)組的類都不是合法的Java類名,,而是由虛擬機(jī)生成,,虛擬機(jī)在生成類是根據(jù)的是實(shí)際構(gòu)造的數(shù)組類型,你構(gòu)造的是Object類型的數(shù)組,,它生成的就是代表Object類型的數(shù)組的類,,無論你把它轉(zhuǎn)型成什么類型,。換句話說,沒有任何方式可以推翻底層數(shù)組的類型,。前面說到,,數(shù)組是協(xié)變的,也就是說[Ljava.lang.Object其實(shí)是[Ljava.lang.String的父類,,比如下面的代碼會得到true:
- String[] array = new String[10];
- System.out.println(array instanceof Object[]);
所以在將rep()返回值賦給String[]類型時(shí),,它確實(shí)是發(fā)生了類型轉(zhuǎn)換,只不過這個(gè)類型轉(zhuǎn)換不是數(shù)組元素的轉(zhuǎn)換,,并不是把Object類型的元素轉(zhuǎn)換成String,,而是把[Ljava.lang.Object轉(zhuǎn)換成了Ljava.lang.String,是父類對象轉(zhuǎn)換成子類,,必然要拋出異常,。那么問題就出來了,我們使用泛型就是為了獲得更加通用的類型,,既然我聲明的是Array<String>,,往里存儲的元素是String,得到的元素也是String,,我理所應(yīng)當(dāng)?shù)恼J(rèn)為,,我獲得的數(shù)組應(yīng)該也是String[],如果我這么做,,你卻給我拋異常,,這是幾個(gè)意思啊,!
導(dǎo)致這個(gè)問題的罪魁還是擦除,,由于擦除,沒有辦法這樣這樣定義數(shù)組:T[] array = new T[size],;為了產(chǎn)生具體類型的數(shù)組,,只能借助于Class對象,在Java類庫提供的Array類提供了一個(gè)創(chuàng)造數(shù)組的方法,,它需要數(shù)組元素類型的Class對象和數(shù)組的長度:
- private Class<T> kind;
- public ArrayMaker(Class<T> kind ) {
- this.kind = kind;
- }
-
- @SuppressWarnings("unchecked")
- T[] create(int size) {
- T[] array = (T[])Array.newInstance(kind, size);
- System.out.println(array.getClass().getName());
- return array;
- }
這樣構(gòu)造的就是具體類型的數(shù)組,,比如傳遞進(jìn)來的是String.class,那么調(diào)用create方法會打?。篬Ljava.lang.String,,在底層構(gòu)造的確實(shí)是String類型的數(shù)組。使用這樣的方式創(chuàng)建數(shù)組,,應(yīng)該是一種更優(yōu)雅,更安全的方式,。
以上內(nèi)容介紹了Java泛型的實(shí)質(zhì),,它的泛型更像是一顆語法糖,,一顆由編譯器包括的語法糖。由編譯器實(shí)現(xiàn)的泛型又有諸多奇怪的限制,,可泛型的功能又是如此強(qiáng)大,,使用的又是如此頻繁,所以對泛型的抱怨一直在持續(xù),,同時(shí),,泛型又是個(gè)繞不過去的彎。
|