開發人員在使用泛型的時候,很容易根據自己的直覺而犯一些錯誤。比如一個方法如果接收List作為形式參數,那么如果嘗試將一個List的對象作為實際參數傳進去,卻發現無法通過編譯。雖然從直覺上來說,Object是String的父類,這種類型轉換應該是合理的。但是實際上這會產生隱含的類型轉換問題,因此編譯器直接就禁止這樣的行為。
類型擦除
正確理解泛型概念的首要前提是理解類型擦除(type erasure)。 Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節代碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會被編譯器在編譯的時候去掉。這個過程就稱為類型擦除。如在代碼中定義的List<Object>和List<String>等類型,在編譯之后都會變成List。JVM看到的只是List,而由泛型附加的類型信息對JVM來說是不可見的。Java編譯器會在編譯時盡可能的發現可能出錯的地方,但是仍然無法避免在運行時刻出現類型轉換異常的情況。類型擦除也是Java的泛型實現方式與C++模板機制實現方式之間的重要區別。
很多泛型的奇怪特性都與這個類型擦除的存在有關,包括:
泛型類并沒有自己獨有的Class類對象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
靜態變量是被泛型類的所有實例所共享的。對于聲明為MyClass<T>的類,訪問其中的靜態變量的方法仍然是 MyClass.myStaticVar。不管是通過new MyClass<String>還是new MyClass<Integer>創建的對象,都是共享一個靜態變量。
泛型的類型參數不能用在Java異常處理的catch語句中。因為異常處理是由JVM在運行時刻來進行的。由于類型信息被擦除,JVM是無法區分兩個異常類型MyException<Integer>和MyException<String>的。對于JVM來說,它們都是 MyException類型的。也就無法執行與異常對應的catch語句。
類型擦除的基本過程也比較簡單,首先是找到用來替換類型參數的具體類。這個具體類一般是Object。如果指定了類型參數的上界的話,則使用這個上界。把代碼中的類型參數都替換成具體的類。同時去掉出現的類型聲明,即去掉<>的內容。比如T get()方法聲明就變成了Object get();List<String>就變成了List。接下來就可能需要生成一些橋接方法(bridge method)。這是由于擦除了類型之后的類可能缺少某些必須的方法。
了解了類型擦除機制之后,就會明白編譯器承擔了全部的類型檢查工作。編譯器禁止某些泛型的使用方式,正是為了確保類型的安全性。
通配符與上下界
在使用泛型類的時候,既可以指定一個具體的類型,如List<String>就聲明了具體的類型是String;也可以用通配符?來表示未知類型,如List<?>就聲明了List中包含的元素類型是未知的。 通配符所代表的其實是一組類型,但具體的類型是未知的。List<?>所聲明的就是所有類型都是可以的。但是List<?>并不等同于List<Object>。List<Object>實際上確定了List中包含的是Object及其子類,在使用的時候都可以通過Object來進行引用。而List<?>則其中所包含的元素類型是不確定。其中可能包含的是String,也可能是 Integer。如果它包含了String的話,往里面添加Integer類型的元素就是錯誤的。正因為類型未知,就不能通過new ArrayList()的方法來創建一個新的ArrayList對象。因為編譯器無法知道具體的類型是什么。但是對于 List<?>中的元素確總是可以用Object來引用的,因為雖然類型未知,但肯定是Object及其子類。
public void wildcard(List<?> list) {list.add(1);//編譯錯誤}
試圖對一個帶通配符的泛型類進行操作的時候,總是會出現編譯錯誤。其原因在于通配符所表示的類型是未知的。(通配符表示的類型未知,可能是String也可能是Float,那我怎么add。要進行類型轉換的,沒法強制啊,隱式轉換又不成功,?改成Object的話那就不一樣了,添加進來int后,父類對象指向子類引用)
對于List<?>中的元素只能用Object來引用,在有些情況下不是很方便。在這些情況下,可以使用上下界來限制未知類型的范圍。 如List<? extends Number>說明List中可能包含的元素類型是Number及其子類。而List<? super Number>則說明List中包含的是Number及其父類。當引入了上界之后,在使用類型的時候就可以使用上界類中定義的方法。比如訪問 List<? extends Number>的時候,就可以使用Number類的intValue等方法。
類型系統
在Java中,大家比較熟悉的是通過繼承機制而產生的類型體系結構。比如String繼承自Object。根據Liskov替換原則,子類是可以替換父類的。當需要Object類的引用的時候,如果傳入一個String對象是沒有任何問題的。但是反過來的話,即用父類的引用替換子類引用的時候,就需要進行強制類型轉換。編譯器并不能保證運行時刻這種轉換一定是合法的。這種自動的子類替換父類的類型轉換機制,對于數組也是適用的。 String[]可以替換Object[]。但是泛型的引入,對于這個類型系統產生了一定的影響。正如前面提到的List<String>是不能替換掉List<Object>的。
引入泛型之后的類型系統增加了兩個維度:一個是類型參數自身的繼承體系結構,另外一個是泛型類或接口自身的繼承體系結構。第一個指的是對于 List<String>和List<Object>這樣的情況,類型參數String是繼承自Object的。而第二種指的是 List接口繼承自Collection接口。對于這個類型系統,有如下的一些規則:
相同類型參數的泛型類的關系取決于泛型類自身的繼承體系結構。即List<String>是Collection<String> 的子類型,List<String>可以替換Collection<String>。這種情況也適用于帶有上下界的類型聲明。
當泛型類的類型聲明中使用了通配符的時候, 其子類型可以在兩個維度上分別展開。如對Collection<? extends Number>來說,其子類型可以在Collection這個維度上展開,即List<? extends Number>和Set<? extends Number>等;也可以在Number這個層次上展開,即Collection<Double>和 Collection<Integer>等。如此循環下去,ArrayList<Long>和 HashSet<Double>等也都算是Collection<? extends Number>的子類型。
如果泛型類中包含多個類型參數,則對于每個類型參數分別應用上面的規則。
理解了上面的規則之后,就可以很容易的修正實例分析中給出的代碼了。只需要把List<?>改成List<Object>即可。List<String>是List<?>的子類型,因此傳遞參數時不會發生錯誤。
開發自己的泛型類
泛型類與一般的Java類基本相同,只是在類和接口定義上多出來了用<>聲明的類型參數。一個類可以有多個類型參數,如 MyClass<X,Y,Z>。 每個類型參數在聲明的時候可以指定上界。所聲明的類型參數在Java類中可以像一般的類型一樣作為方法的參數和返回值,或是作為域和局部變量的類型。但是由于類型擦除機制,類型參數并不能用來創建對象或是作為靜態變量的類型。
class ClassTest<X extends Number,Y,Z> {
private X x;
private static Y y; //編譯錯誤,不能用在靜態變量中(為什么呢?)
public X getFirst() {//正確用法
return x;}
public void wrong() {Z z = new Z(); //編譯錯誤,不能創建對象
}}
在代碼中避免泛型類和原始類型的混用。比如List<String>和List不應該共同使用。這樣會產生一些編譯器警告和潛在的運行時異常。
在使用帶通配符的泛型類的時候,需要明確通配符所代表的一組類型的概念。由于具體的類型是未知的,很多操作是不允許的。
泛型類最好不要同數組一塊使用。你只能創建new List<?>[10]這樣的數組,無法創建new List<String>[10]這樣的。這限制了數組的使用能力,而且會帶來很多費解的問題。因此,當需要類似數組的功能時候,使用集合類即可。
不要忽視編譯器給出的警告信息。
一. 泛型概念的提出(為什么需要泛型)?
什么辦法可以使集合能夠記住集合內元素各類型,且能夠達到只要編譯時不出現問題,運行時就不會出現“java.lang.ClassCastException”異常呢?答案就是使用泛型。
二.什么是泛型?
泛型,即“參數化類型”。一提到參數,最熟悉的就是定義方法時有形參,然后調用此方法時傳遞實參。那么參數化類型怎么理解呢?顧名思義,就是將類型由原來的具體類型進行參數化,類似于方法中的變量參數,此時類型也定義成參數形式(可以稱之為類型形參),然后在使用/調用時傳入具體的類型(類型實參)。
我們可以看到,在List接口中采用泛型化定義之后,中的E表示類型形參,可以接收具體的類型實參,并且此接口定義中,凡是出現E的地方均表示相同的接受自外部的類型實參。
三.自定義泛型接口、泛型類和泛型方法
接口、類和方法也都可以使用泛型去定義,以及相應的使用。是的,在具體使用時,可以分為泛型接口、泛型類和泛型方法。自定義泛型接口、泛型類和泛型方法與上述Java源碼中的List、ArrayList類似。如下,我們看一個最簡單的泛型類和方法定義:
public class GenericTest {? ? public static void main(String[] args) {? ? ? ? Boxname = new Box("corn");? ? ? ? System.out.println("name:" + name.getData());? ? }}class Box{private T data;
public Box() {}
public Box(T data) {this.data = data;}
public T getData() {return data;}
}
在泛型接口、泛型類和泛型方法的定義過程中,我們常見的如T、E、K、V等形式的參數常用于表示泛型形參,由于接收來自外部使用時候傳入的類型實參。
那么對于不同傳入的類型實參,生成的相應對象實例的類型是不是一樣的呢?
public class GenericTest {? ? public static void main(String[] args) {? ? ? ? Boxname = new Box("corn");? ? ? ? Boxage = new Box(712);
System.out.println("name class:" + name.getClass());? ? ? // com.*.Box
System.out.println("age class:" + age.getClass());? ? ? ? // com.*.Box
System.out.println(name.getClass() == age.getClass());? ? // true}}
由此,我們發現,在使用泛型類時,雖然傳入了不同的泛型實參,但并沒有真正意義上生成不同的類型,傳入不同泛型實參的泛型類在內存上只有一個,即還是原來的最基本的類型(本實例中為Box),當然,在邏輯上我們可以理解成多個不同的泛型類型。
究其原因,在于Java中的泛型這一概念提出的目的,導致其只是作用于代碼編譯階段,在編譯過程中,對于正確檢驗泛型結果后,會將泛型的相關信息擦出,也就是說,成功編譯過后的class文件中是不包含任何泛型信息的。泛型信息不會進入到運行時階段。
對此總結成一句話:泛型類型在邏輯上可以看成是多個不同的類型,實際上都是相同的基本類型。
四.類型通配符
接著上面的結論,我們知道,Box和Box實際上都是Box類型,現在需要繼續探討一個問題,那么在邏輯上,類似于Box和Box是否可以看成具有父子關系的泛型類型呢?
為了弄清這個問題,我們繼續看下下面這個例子
public class GenericTest {? ? public static void main(String[] args) {? ? ? ? Boxname = new Box(99);? ? ? ? Boxage = new Box(712);? ? ? ? getData(name);? ? ? ? ? ? ? ? //The method getData(Box) in the type GenericTest is? ? ? ? //not applicable for the arguments (Box)? ? ? ? getData(age);? // 1? ? }? ? ? ? public static void getData(Boxdata){System.out.println("data :" + data.getData());}}
我們發現,在代碼//1處出現了錯誤提示信息:The method getData(Box) in the t ype GenericTest is not applicable for the arguments (Box)。顯然,通過提示信息,我們知道Box在邏輯上不能視為Box的父類。由于在編程過程中的順序不可控性,導致在必要的時候必須要進行類型判斷,且進行強制類型轉換。顯然,這與泛型的理念矛盾,因此,在邏輯上Box<Number>不能視為Box<Integer>的父類。
在邏輯上可以用來表示同時是Box和Box的父類的一個引用類型,由此,類型通配符應運而生。類型通配符一般是使用 ? 代替具體的類型實參。注意了,此處是類型實參,而不是類型形參!且Box<?>在邏輯上是Box<Integer>、Box<Number>...等所有Box<具體類型實參>的父類。由此,我們依然可以定義泛型方法,來完成此類需求。有時候,我們還可能聽到類型通配符上限和類型通配符下限。具體有是怎么樣的呢?如果需要定義一個功能類似于getData()的方法,但對類型實參又有進一步的限制:只能是Number類及其子類。此時,需要用到類型通配符上限。
public static void getData(Box<? extends Number> data) {System.out.println("data :" +data.getData());}
類型通配符上限通過形如Box<? extends >形式定義,相對應的,類型通配符下限為Box<? super >形式,其含義與類型通配符上限正好相反。
泛型不是協變的
雖然將集合看作是數組的抽象會有所幫助,但是數組還有一些集合不具備的特殊性質。Java 語言中的數組是協變的(covariant),也就是說,如果Integer擴展了Number(事實也是如此),那么不僅Integer是Number,而且Integer[]也是Number[],在要求Number[]的地方完全可以傳遞或者賦予Integer[]。(更正式地說,如果Number是Integer的超類型,那么Number[]也是Integer[]的超類型)。您也許認為這一原理同樣適用于泛型類型 ——List<Number>是List<Integer>的超類型,那么可以在需要List<Number>的地方傳遞List<Integer>。不幸的是,情況并非如此。
不允許這樣做有一個很充分的理由:這樣做將破壞要提供的類型安全泛型。如果能夠將List賦給List。那么下面的代碼就允許將非Integer的內容放入List:
List<Integer>?li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));
因為ln是List,所以向其添加Float似乎是完全合法的。但是如果ln是li的別名,那么這就破壞了蘊含在li定義中的類型安全承諾 —— 它是一個整數列表,這就是泛型類型不能協變的原因。
其他的協變問題
數組能夠協變而泛型不能協變的另一個后果是,不能實例化泛型類型的數組(new List<String>[3]是不合法的),除非類型參數是一個未綁定的通配符(new List<?>[3]是合法的)。讓我們看看如果允許聲明泛型類型數組會造成什么后果:
List<String>[] lsa = new List<String>[10]; // illegal ?數組能夠協變,泛型不能
List<?>[] lsa = new List<?>[10]; // legal ? 泛型不能協變 ?不確定具體的類型
Object[] oa = lsa;? // OK because List is a subtype of Object
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[0] = li;
String s = lsa[0].get(0);
最后一行將拋出ClassCastException,因為這樣將把List填入本應是List的位置。因為數組協變會破壞泛型的類型安全,所以不允許實例化泛型類型的數組(除非類型參數是未綁定的通配符,比如List)。