35節介紹了泛型的基本概念和原理,上節介紹了泛型中的通配符,本節來介紹泛型中的一些細節和局限性。
這些局限性主要與Java的實現機制有關,Java中,泛型是通過類型擦除來實現的,類型參數在編譯時會被替換為Object,運行時Java虛擬機不知道泛型這回事,這帶來了很多局限性,其中有的部分是比較容易理解的,有的則是非常違反直覺的。
一項技術,往往只有理解了其局限性,我們才算是真正理解了它,才能更好的應用它。
下面,我們將從以下幾個方面來介紹這些細節和局限性:
使用泛型類、方法和接口
在使用泛型類、方法和接口時,有一些值得注意的地方,比如:
我們逐個來看下。
基本類型不能用於實例化類型參數
Java中,因為類型參數會被替換為Object,所以Java泛型中不能使用基本數據類型,也就是說,類似下面寫法是不合法的:
Pair<int> minmax = new Pair<int>(1,100);
解決方法就是使用基本類型對應的包裝類。
運行時類型信息不適用於泛型
在介紹繼承的實現原理時,我們提到,在內存中,每個類都有一份類型信息,而每個對象也都保存著其對應類型信息的引用。關於運行時信息,後續文章我們會進一步詳細介紹,這裡簡要說明一下。
在Java中,這個類型信息也是一個對象,它的類型為Class,Class本身也是一個泛型類,每個類的類型對象可以通過<類名>.class的方式引用,比如String.class,Integer.class。
這個類型對象也可以通過對象的getClass()方法獲得,比如:
Class<?> cls = "hello".getClass();
這個類型對象只有一份,與泛型無關,所以Java不支持類似如下寫法:
Pair<Integer>.class
一個泛型對象的getClass方法的返回值與原始類型對象也是相同的,比如說,下面代碼的輸出都是true:
Pair<Integer> p1 = new Pair<Integer>(1,100); Pair<String> p2 = new Pair<String>("hello","world"); System.out.println(Pair.class==p1.getClass()); System.out.println(Pair.class==p2.getClass());
在第16節,我們介紹過instanceof關鍵字,instanceof後面是接口或類名,instanceof是運行時判斷,也與泛型無關,所以,Java也不支持類似如下寫法:
if(p1 instanceof Pair<Integer>)
不過,Java支持這麼寫:
if(p1 instanceof Pair<?>)
類型擦除可能會引發一些沖突
由於類型擦除,可能會引發一些編譯沖突,這些沖突初看上去並不容易理解,我們通過一些例子看一下。
上節我們介紹過一個例子,有兩個類Base和Child,Base的聲明為:
class Base implements Comparable<Base>
Child的聲明為:
class Child extends Base
Child沒有專門實現Comparable接口,上節我們說Base類已經有了比較所需的全部信息,所以Child沒有必要實現,可是如果Child希望自定義這個比較方法呢?直覺上,可以這樣修改Child類:
class Child extends Base implements Comparable<Child>{ @Override public int compareTo(Child o) { } //... }
遺憾的是,Java編譯器會提示錯誤,Comparable接口不能被實現兩次,且兩次實現的類型參數還不同,一次是Comparable<Base>,一次是Comparable<Child>。為什麼不允許呢?因為類型擦除後,實際上只能有一個。
那Child有什麼辦法修改比較方法呢?只能是重寫Base類的實現,如下所示:
class Child extends Base { @Override public int compareTo(Base o) { if(!(o instanceof Child)){ throw new IllegalArgumentException(); } Child c = (Child)o; //... return 0; } //... }
還有,你可能認為可以這麼定義重載方法:
public static void test(DynamicArray<Integer> intArr) public static void test(DynamicArray<String> strArr)
雖然參數都是DynamicArray,但實例化類型不同,一個是DynamicArray<Integer>,另一個是DynamicArray<String>,同樣,遺憾的是,Java不允許這種寫法,理由同樣是,類型擦除後,它們的聲明是一樣的。
定義泛型類、方法和接口
在定義泛型類、方法和接口時,也有一些需要注意的地方,比如:
我們逐個來看下。
不能通過類型參數創建對象
不能通過類型參數創建對象,比如,T是類型參數,下面寫法都是非法的:
T elm = new T(); T[] arr = new T[10];
為什麼非法呢?因為如果允許,那你以為創建的就是對應類型的對象,但由於類型擦除,Java只能創建Object類型的對象,而無法創建T類型的對象,容易引起誤解,所以Java干脆禁止這麼做。
那如果確實希望根據類型創建對象呢?需要設計API接受類型對象,即Class對象,並使用Java中的反射機制,後續文章我們再詳細介紹反射,這裡簡要說明一下,如果類型有默認構造方法,可以調用Class的newInstance方法構建對象,類似這樣:
public static <T> T create(Class<T> type){ try { return type.newInstance(); } catch (Exception e) { return null; } }
比如:
Date date = create(Date.class); StringBuilder sb = create(StringBuilder.class);
泛型類類型參數不能用於靜態變量和方法
對於泛型類聲明的類型參數,可以在實例變量和方法中使用,但在靜態變量和靜態方法中是不能使用的。類似下面這種寫法是非法的:
public class Singleton<T> { private static T instance; public synchronized static T getInstance(){ if(instance==null){ // 創建實例 } return instance; } }
如果合法的話,那麼對於每種實例化類型,都需要有一個對應的靜態變量和方法。但由於類型擦除,Singleton類型只有一份,靜態變量和方法都是類型的屬性,且與類型參數無關,所以不能使用泛型類類型參數。
不過,對於靜態方法,它可以是泛型方法,可以聲明自己的類型參數,這個參數與泛型類的類型參數是沒有關系的。
了解多個類型限定的語法
之前介紹類型參數限定的時候,我們介紹,上界可以為某個類、某個接口或者其他類型參數,但上界都是只有一個,Java中還支持多個上界,多個上界之間以&分隔,類似這樣:
T extends Base & Comparable & Serializable
Base為上界類,Comparable和Serializable為上界接口,如果有上界類,類應該放在第一個,類型擦除時,會用第一個上界替換。
泛型與數組
泛型與數組的關系稍微復雜一些,我們單獨討論一下。
為什麼不能創建泛型數組?
引入泛型後,一個令人驚訝的事實是,你不能創建泛型數組。比如說,我們可能想這樣創建一個Pair的泛型數組,以表示隨機一節中介紹的獎勵面額和權重。
Pair<Object,Integer>[] options = new Pair<Object,Integer>[]{ new Pair("1元",7), new Pair("2元", 2), new Pair("10元", 1) };
Java會提示編譯錯誤,不能創建泛型數組。這是為什麼呢?我們先來進一步理解一下數組。
前面我們解釋過,類型參數之間有繼承關系的容器之間是沒有關系的,比如,一個DynamicArray<Integer>對象不能賦值給一個DynamicArray<Number>變量。不過,數組是可以的,看代碼:
Integer[] ints = new Integer[10]; Number[] numbers = ints; Object[] objs = ints;
後面兩種賦值都是允許的。數組為什麼可以呢?數組是Java直接支持的概念,它知道數組元素的實際類型,它知道Object和Number都是Integer的父類型,所以這個操作是允許的。
雖然Java允許這種轉換,但如果使用不當,可能會引起運行時異常,比如:
Integer[] ints = new Integer[10]; Object[] objs = ints; objs[0] = "hello";
編譯是沒有問題的,運行時會拋出ArrayStoreException,因為Java知道實際的類型是Integer,所以寫入String會拋出異常。
理解了數組的這個行為,我們再來看泛型數組。如果Java允許創建泛型數組,則會發生非常嚴重的問題,我們看看具體會發生什麼:
Pair<Object,Integer>[] options = new Pair<Object,Integer>[3]; Object[] objs = options; objs[0] = new Pair<Double,String>(12.34,"hello");
如果可以創建泛型數組options,那它就可以賦值給其他類型的數組objs,而最後一行明顯錯誤的賦值操作,則既不會引起編譯錯誤,也不會觸發運行時異常,因為Pair<Double,String>的運行時類型是Pair,和objs的運行時類型Pair[]是匹配的。但我們知道,它的實際類型是不匹配的,在程序的其他地方,當把objs[0]當做Pair<Object,Integer>進行處理的時候,一定會觸發異常。
也就是說,如果允許創建泛型數組,那就可能會有上面這種錯誤操作,它既不會引起編譯錯誤,也不會立即觸發運行時異常,卻相當於埋下了一顆炸彈,不定什麼時候爆發,為避免這種情況,Java干脆就禁止創建泛型數組。
如何存放泛型對象?
但,現實需要能夠存放泛型對象的容器啊,怎麼辦呢?可以使用原始類型的數組,比如:
Pair[] options = new Pair[]{ new Pair<String,Integer>("1元",7), new Pair<String,Integer>("2元", 2), new Pair<String,Integer>("10元", 1)};
更好的選擇是,使用後續章節介紹的泛型容器。目前,可以使用我們自己實現的DynamicArray,比如:
DynamicArray<Pair<String,Integer>> options = new DynamicArray<>(); options.add(new Pair<String,Integer>("1元",7)); options.add(new Pair<String,Integer>("2元",2)); options.add(new Pair<String,Integer>("10元",1));
DynamicArray內部的數組為Object類型,一些操作插入了強制類型轉換,外部接口是類型安全的,對數組的訪問都是內部代碼,可以避免誤用和類型異常。
如何轉換容器為數組?
有時,我們希望轉換泛型容器為一個數組,比如說,對於DynamicArray,我們可能希望它有這麼一個方法:
public E[] toArray()
而我們希望可以這麼用:
DynamicArray<Integer> ints = new DynamicArray<Integer>(); ints.add(100); ints.add(34); Integer[] arr = ints.toArray();
先使用動態容器收集一些數據,然後轉換為一個固定數組,這也是一個常見合理的需求,怎麼來實現這個toArray方法呢?
可能想先這樣:
E[] arr = new E[size];
遺憾的是,如之前所述,這是不合法的。Java運行時根本不知道E是什麼,也就無法做到創建E類型的數組。
另一種想法是這樣:
public E[] toArray(){ Object[] copy = new Object[size]; System.arraycopy(elementData, 0, copy, 0, size); return (E[])copy; }
或者使用之前介紹的Arrays方法:
public E[] toArray(){ return (E[])Arrays.copyOf(elementData, size); }
結果都是一樣的,沒有編譯錯誤了,但運行時,會拋出ClassCastException異常,原因是,Object類型的數組不能轉換為Integer類型的數組。
那怎麼辦呢?可以利用Java中的運行時類型信息和反射機制,這些概念我們後續章節再介紹。這裡,我們簡要介紹下。
Java必須在運行時知道你要轉換成的數組類型,類型可以作為參數傳遞給toArray方法,比如:
public E[] toArray(Class<E> type){ Object copy = Array.newInstance(type, size); System.arraycopy(elementData, 0, copy, 0, size); return (E[])copy; }
Class<E>表示要轉換成的數組類型信息,有了這個類型信息,Array類的newInstance方法就可以創建出真正類型的數組對象。
調用toArray方法時,需要傳遞需要的類型,比如,可以這樣:
Integer[] arr = ints.toArray(Integer.class);
泛型與數組小結
我們來稍微總結下泛型與數組的關系:
小結
本節介紹了泛型的一些細節和局限性,這些局限性主要是由於Java泛型的實現機制引起的,這些局限性包括,不能使用基本類型,沒有運行時類型信息,類型擦除會引發一些沖突,不能通過類型參數創建對象,不能用於靜態變量等,我們還單獨討論了泛型與數組的關系。
我們需要理解這些局限性,但,幸運的是,一般並不需要特別去記憶,因為用錯的時候,Java開發環境和編譯器會提示你,當被提示時,你需要能夠理解,並可以從容應對。
至此,關於泛型的介紹就結束了,泛型是Java容器類的基礎,理解了泛型,接下來,就讓我們開始探索Java中的容器類。
----------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。