相信通過上一篇泛型相關的文章,大家對泛型有了一個大緻的了解,現在我們來簡單的看一個小例子:
public class GenericEraseTest {
public static void main(String[] args){
ArrayList<String> stringList = new ArrayList<String>();
ArrayList<Integer> intList = new ArrayList<Integer>();
System.out.println(stringList.getClass() == intList.getClass());
}
}
上面的代碼列印出的應該是什麼?根據我們上一次泛型基礎所了解到的,int類型的元素是無法添加到stringList中的,按正常的思維,列印出的值應該是false,因為很明顯兩個類的行為不同(接受的參數類型不同)。但是結局又是讓人崩潰的,列印出了true。我們将上面的java代碼編譯成class檔案,然後再反編譯出來結果如下所示:
public class GenericEraseTest {
public static void main(String[] args){
ArrayList stringList = new ArrayList();
ArrayList intList = new ArrayList();
System.out.println(stringList.getClass() == intList.getClass());
}
}
現在看起來應該熟悉多了,列印出的true也應該是在意料之中了。但是為什麼這樣?這就引出了今天的第一個概念:
泛型的擦除
在java語言中,泛型隻存在于源代碼中,而在位元組碼中泛型類都被替換為原生類,在運作期所操作的類型也都是原生類,這種特性我們稱之為擦除。相信有了上面的執行個體,不難了解這句話的意思。我們來想一下java的泛型為什麼通過擦除來實作?
在C++或者C#中,泛型無論是在源碼,還是在編譯的中間代碼,亦或者是在運作期中,泛型都是真實存在的,我們都可以正常的使用它,List<String>和List<Integer>就是兩個不同的類,但是在java中并不是這樣的。關于在java中為什麼利用擦除來實作泛型我了解的有大概兩種說法:
1.對相容性方面的考慮。在Thinking in java 一書中作者說了如下一段話:
“為了減少潛在的關于擦除的混淆,你必須清楚的認識到這不是一個語言特性,它是java的泛型實作中的一種折中。如果泛型在Java1.0中就已經是其一部分了,那麼這個特性将不會用擦除來實作——它将使用具體化”
在java1.5以後的版本中,即使引入了泛型的概念,我們也必須使其能相容之前在沒有泛型時所編寫的類庫。而之前所寫的代碼也要能在泛型加入類庫中去時繼續保持可用。
2.由于在C++或C#中泛型是真實存在的,List<String>和List<Integer>将生成兩個不同的類,這樣很容易導緻類膨脹的問題,使得代碼編譯的速度降低。
上面兩種說法都有自己的道理,也無法去深究其對錯,而我們要做的是了解它本質的含義,以便在使用時可以得心應手。
泛型擦除所帶來的影響
在泛型代碼的内部,我們無法獲得任何有關泛型參數類型的資訊,雖然能得到類型的參數辨別,但是并不能用來建立執行個體。這句話看起來比較抽象,什麼是參數類型資訊,什麼是參數辨別還是一頭霧水,沒關系,我們看下面幾個例子:
public T get() {
T t = new T();
return a;
}
如果我們在代碼中寫了類似上面的語句,那麼編譯器報錯,并且提示如下的語句“Cannot instantiate the type T”。
if(T instanceof String){
//xxxxx
}
如果我們在代碼中這樣寫,編譯器同樣也會報錯“T cannot be resolved”。
現在大家應該可以明白上面所說的不能獲得任何參數類型的資訊,也不能用來建立執行個體hi什麼意思了吧。但是将其作為類型來轉型還是可以的,比如說這樣:
public T get() {
Object obj = new Object();
return (T)obj;
}
編譯器隻是報了一個轉型的警告但是并沒有阻止,這段代碼也解釋了上面提到的可以獲得類型的參數辨別。
為了分析内部的原因,我們引入一個簡單的Holder類代碼:
public class Holder<T> {
private T a;
public Holder(T a) {
this.a = a;
}
public void set(T a) {
this.a = a;
}
public T get() {
return a;
}
public static void main(String[] args) {
Holder<String> holder = new Holder<String>("123");
String string = holder.get();
System.out.println(string);
}
}
以下是反編譯出來的執行過程:
Compiled from "Holder.java"
public class com.fsc.generic.Holder<T> {
public com.fsc.generic.Holder(T);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: aload_0
5: aload_1
6: putfield #2 // Field a:Ljava/lang/Object;
9: return
public void set(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field a:Ljava/lang/Object;
5: return
public T get();
Code:
0: aload_0
1: getfield #2 // Field a:Ljava/lang/Object;
4: areturn
public static void main(java.lang.String[]);
Code:
0: new #3 // class com/fsc/generic/Holder
3: dup
4: ldc #4 // String 123
6: invokespecial #5 // Method "<init>":(Ljava/lang/Objec
t;)V
9: astore_1
10: aload_1
11: invokevirtual #6 // Method get:()Ljava/lang/Object;
14: checkcast #7 // class java/lang/String
17: astore_2
18: getstatic #8 // Field java/lang/System.out:Ljava/
io/PrintStream;
21: aload_2
22: invokevirtual #9 // Method java/io/PrintStream.printl
n:(Ljava/lang/String;)V
25: return
}
我們可以從中看到兩點比較重要的内容:1.在調用set方法傳遞參數的時候是以Object對象來接收的。2.在調用get方法進行傳回時傳回的是Object,仍然需要轉型,隻不過轉型不是手動的而已,是編譯器自動幫我們插入的。通過上面的代碼我們也應該明白了為什麼不能使用new關鍵字來建立T類型的對象了,因為T根本就不存在,在類中針對T的方法操作其實都是針對Object來的。
在對泛型的使用中,我們失去了它的參數類型資訊,不能用來建立對象以及類型的比較,接下來提供一種思路來處理這種問題:
public class Holder<T> {
private Class<T> cls;
public Holder(Class<T> cls) {
this.cls = cls;
}
public T getInstance(){
T newInstance = null;
try {
newInstance = cls.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return newInstance;
}
public boolean isInstance(Object obj){
return cls.isInstance(obj);
}
public static void main(String[] args) {
Holder<String> holder = new Holder<String>(String.class);
boolean isInstance = holder.isInstance("123");
System.out.println(isInstance);
String instance = holder.getInstance();
System.out.println(instance);
}
}
我們重新定義了一個Holder類,隻不過它存儲的東西程式設計了具體類型的Class對象,這樣我們就能通過這個Class對象來在一定程度上來彌補泛型類所帶來的缺陷。當然這也僅僅隻是一段示例,若要真正使用,還需要處理很多問題。
泛型數組的建立
通過前面的學習我們知道,在泛型内無法得到泛型參數類型的資訊,那麼我們如何建立出泛型參數類型的數組呢?
由于泛型的類型資訊在運作期被擦除掉了,在有泛型類型參與的地方全部變為Object(當然也有可能是其他的類,在下一篇文章中會介紹),那麼我們是不是可以考慮建立出一個Object類型的數組,然後将其轉型儲存起來,就像下面這樣:
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int size){
array = (T[]) new Object[size];
}
public T[] getArray(){
return array;
}
public static void main(String[] args) {
GenericArray<String> genericArray = new GenericArray<String>(2);
String[] array2 = genericArray.getArray();
}
}
在建立數組的時候因為涉及到了轉型資訊,是以使用注解抑制了警告。運作上面的程式将會發現報錯了,錯誤如下:
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
錯誤已經說的很清楚了,不能将Object類型的數組轉換成String類型的數組
我們再來看另外一種寫法:
public class GenericArray<T> {
private Object[] array;
public GenericArray(int size){
array = new Object[size];
}
@SuppressWarnings("unchecked")
public T[] getArray(){
return (T[]) array;
}
public static void main(String[] args) {
GenericArray<String> genericArray = new GenericArray<String>(2);
String[] array2 = genericArray.getArray();
}
}
将轉型的位置換了地方,在泛型數組中用Object數組來存放資料,但是很不幸,仍然報了和剛才一樣的錯誤。雖然這種寫法仍然報錯,但是如果仔細檢視java的源碼就會發現ArrayList使用的就是這種方式,盡管它沒有向我們提供接口來傳回内部的數組。
下面再來看一種寫法:
public class GenericArray<T> {
private T[] array;
private Class cls;
@SuppressWarnings("unchecked")
public GenericArray(int size, Class cls){
this.cls = cls;
array = (T[]) Array.newInstance(cls, size);
}
public T[] getArray(){
return array;
}
public static void main(String[] args) {
GenericArray<String> genericArray = new GenericArray<String>(2,String.class);
String[] array2 = genericArray.getArray();
}
}
運作結果一切正常,使用這種方式來建立泛型數組是可取的。再者,在我們想使用泛型數組的時候可以直接使用容器,容器也是支援泛型的,是以和使用數組的感覺沒什麼太大的不同。
在下一篇文章中将會詳細的介紹泛型的邊界,通配符,以及一些總結。