天天看點

java泛型(二)--泛型的擦除

相信通過上一篇泛型相關的文章,大家對泛型有了一個大緻的了解,現在我們來簡單的看一個小例子:

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();
	}
}
           

運作結果一切正常,使用這種方式來建立泛型數組是可取的。再者,在我們想使用泛型數組的時候可以直接使用容器,容器也是支援泛型的,是以和使用數組的感覺沒什麼太大的不同。

在下一篇文章中将會詳細的介紹泛型的邊界,通配符,以及一些總結。

繼續閱讀