Java1.5版本中增加了泛型。在沒有泛型之前,從集合中讀取到的每一個(gè)對象都必須進(jìn)行轉(zhuǎn)換。如果不小心插入了錯(cuò)誤類型的對象,在運(yùn)行時(shí)的轉(zhuǎn)換處理就會(huì)出錯(cuò)。有了泛型之后,可以告訴編譯器每個(gè)集合中可以接受哪些對象類型,編譯器自動(dòng)為插入操作進(jìn)行轉(zhuǎn)換,并在編譯時(shí)告知是否插入了類型錯(cuò)誤的對象,使得程序更加安全,代碼意圖也更加清楚。
由編譯器進(jìn)行類型匹配與檢查,若對象類型錯(cuò)誤,編譯時(shí)即可發(fā)現(xiàn)錯(cuò)誤,而非要等到運(yùn)行時(shí)。
本章內(nèi)容導(dǎo)圖:

1.不要在新代碼中使用原生態(tài)類型
泛型類/接口:聲明中具有一個(gè)或者多個(gè)類型參數(shù)的類/接口
每種泛型定義一組參數(shù)化的類型,如List<String>,讀作字符串列表,表示元素類型為String的列表。
每個(gè)泛型都定義一個(gè)原生態(tài)類型,即不帶任何實(shí)際類型參數(shù)的泛型名稱。如與List<E>相對應(yīng)的原生態(tài)類型就是List。
原生態(tài)類型沒有泛型在安全性和表述性方面的優(yōu)勢,它的存在僅是為了兼容引入泛型之前的遺留代碼,不應(yīng)在新代碼中繼續(xù)使用。
//使用原生態(tài)類型
private final List stamps = new ArrayList();
stamps.add( new Stamp() );
stamps.add( new Coin() ); //可以正常添加
Stamp stamp = (Stamp)stamps.get(1); //運(yùn)行時(shí)錯(cuò)誤,拋出ClassCastException。
//使用泛型
private final List<Stamp> stamps = new ArrayList<Stamp>();
stamps.add( new Stamp() );
stamps.add( new Coin() ); //提示錯(cuò)誤,無法通過編譯
Stamp stamp = stamps.get(0); //使用時(shí)無需進(jìn)行手工轉(zhuǎn)換
由上述代碼可以看出,使用泛型的兩個(gè)好處為:
1.由編譯器確保插入正確的元素類型
2.從集合獲取元素時(shí)不再需要手工轉(zhuǎn)換了
如果要使用泛型,但不確定或不關(guān)心實(shí)際的類型參數(shù),可以使用一個(gè)?代替,稱作無限制的通配符類型,如泛型Set<E>的無限制通配符類型為Set<?>,讀作某個(gè)類型的集合。通配符類型是安全的,原生態(tài)類型不安全。
不在新代碼中使用原生態(tài)類型這條規(guī)則有兩種例外情況:
1.在類文字中必須使用原生態(tài)類型
//正確的用法
List.class
String[].class
int.class
//錯(cuò)誤的用法
List<String.class>
List<?>.class
2.在instanceof操作符中必須使用原生態(tài)類型
if (o instanceof Set) {
Set<?> m = (Set<?>)o;
}
上述兩種例外都是源于泛型信息可以在運(yùn)行時(shí)被擦除。
使用原生態(tài)類型會(huì)在運(yùn)行時(shí)導(dǎo)致異常,因此不要在新代碼中使用。
原生態(tài)類型只是為了與引入泛型之前的遺留代碼進(jìn)行兼容和互用而提供的。
Set<Object>是個(gè)參數(shù)化類型,表示可以保護(hù)任意對象類型的一個(gè)集合;
Set<?>則是一個(gè)通配符類型,表示只能包含某種未知對象類型的一個(gè)集合;
Set則是個(gè)原生態(tài)類型。
2.消除非受檢警告
用泛型編程時(shí),會(huì)遇到很多編譯器警告:
非受檢強(qiáng)制轉(zhuǎn)換警告
非受檢方法調(diào)用警告
非受檢普通數(shù)組創(chuàng)建警告
非受檢轉(zhuǎn)換警告
要盡可能地消除每一個(gè)非受檢警告,這可以確保代碼是類型安全的,意味著代碼在運(yùn)行時(shí)不會(huì)出現(xiàn)ClassCastException異常。
現(xiàn)代IDE工具都會(huì)提示這種警告信息,使用泛型時(shí),如有這種非受檢警告,按照IDE工具的提示逐個(gè)消除就可以了。
//含警告信息的泛型使用
Set<Lark> exaltation = new HashSet();
//消除警告信息的、類型安全的泛型使用
Set<Lark> exaltation = new HashSet<Lark>();
SuppressWarnings注解可以用在任何粒度的級別中,從單獨(dú)的局部變量聲明到整個(gè)類的定義都可以。應(yīng)該始終在盡可能小的范圍中使用SuppressWarnings注解,永遠(yuǎn)不要在整個(gè)類上使用SuppressWarnings,因?yàn)檫@么做可能會(huì)掩蓋重要的警告信息。
每當(dāng)使用SuppressWarnings("unchecked")注解時(shí),都要添加一條注釋,說明為什么這么做是安全的。這樣做可以幫助他人理解代碼,更重要的是,可以盡量減少其他人修改代碼后導(dǎo)致計(jì)算不安全的概率。
非受檢警告很重要,不要忽略它們。每一條警告都表示可能在運(yùn)行時(shí)拋出ClassCastException異常,要盡最大的努力消除這些警告。如果無法消除非受檢警告,同時(shí)又足以證明引起警告的代碼是類型安全的,就可以在盡可能小的范圍中,用@SuppressWarnings("unchecked")注解禁止該警告,并把禁止該警告的原因注釋記錄下來。
3.列表優(yōu)先于數(shù)組
數(shù)組與泛型相比,有兩個(gè)重要的不同點(diǎn):
1.數(shù)組是協(xié)變的
協(xié)變指的是如果Sub為Super的子類型,那么數(shù)組類型Sub[]就是Super[]的子類型;
泛型是不可變的,對于任意兩個(gè)不同的類型Type1和Type2,List<Type1>既不是List<Type2>的子類型,也不是List<Type2>的超類型。
2.數(shù)組是具體化的
數(shù)組在運(yùn)行時(shí)才知道并檢查它們的元素類型約束;
泛型則通過類型擦除來實(shí)現(xiàn),它在編譯時(shí)強(qiáng)化它們的類型信息,在運(yùn)行時(shí)丟棄(或擦除)它們的元素類型信息。
由于數(shù)組的協(xié)變性和具體化,它是有缺陷的:
//數(shù)組具有協(xié)變性,Object是Long的父類,聲明合法
Object[] objectArray = new Long[1];
//Long[] 退化為Object[],此處賦值也是合法的
objectArray[0] = "I don't fit in";
上述代碼可以通過編譯,但運(yùn)行時(shí)卻拋出ArrayStoreException。
改為列表后,則無法通過編譯時(shí)的類型檢查:
//無法通過編譯,List<Object>和List<Long>是不同的類型
List<Object> ol = new ArrayList<Long>();
ol.add("I don't fit in");
因?yàn)閿?shù)組和泛型之間有著根本性的區(qū)別,數(shù)組和泛型不能很好地混合使用。如下列類型的表達(dá)式都是非法的:new List<E>[]、new List<String>[]、new E[]。
創(chuàng)建泛型數(shù)組是非法的,是因?yàn)榉盒蛿?shù)組不是類型安全的。如下代碼所示:
List<String>[] strLists = new List<String>[1]; //假設(shè)此處合法
List<Integer> intList = Arrays.asList(42);
Object[] objects = strLists; //數(shù)組是協(xié)變的,此處合法
objects[0] = intList;
String s = strLists[0].get(0); //運(yùn)行時(shí)ClassCastException異常
當(dāng)?shù)玫椒盒蛿?shù)組創(chuàng)建錯(cuò)誤時(shí),最好的解決辦法通常是優(yōu)先使用集合類型List<E>,而不是數(shù)組類型E[]。這樣可能會(huì)損失一些性能或簡潔性,但換回的卻是更高的類型安全性和互用性。
數(shù)組和泛型有著非常不同的類型規(guī)則。數(shù)組是協(xié)變且可以具體化的;泛型是不可變的且可以被擦除的。因此,數(shù)組提供了運(yùn)行時(shí)的類型安全,但沒有編譯時(shí)的類型安全,對于泛型也一樣。
一般來說,數(shù)組和泛型不能很好地混合使用,如果將它們混合使用,且得到了編譯器的錯(cuò)誤或警告,第一反應(yīng)就應(yīng)該是用列表代替數(shù)組。
4.優(yōu)先考慮泛型
編寫自己的泛型相對比較困難,但很值得花時(shí)間去學(xué)習(xí)如何編寫。
下面以一個(gè)Stack類為例來說明:
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * DEFAULT_INITIAL_CAPACITY + 1);
}
}
}
上述Stack類的實(shí)現(xiàn),主要問題有如下兩點(diǎn):
1.push操作無法保證類型安全
//可以向stack中放入任意類型
Stack stack = new Stack();
stack.push("stack");
stack.put(new Integer(100));
2.pop操作獲得元素需要外部手工進(jìn)行類型轉(zhuǎn)換,且可能會(huì)產(chǎn)生ClassCastException異常。
String str = (String)stack.pop();
將上述Stack類進(jìn)行泛型化,主要步驟為:
1.給它的聲明添加一個(gè)或者多個(gè)類型參數(shù)
2.用相應(yīng)的類型參數(shù)替換所有的Object類型,嘗試編譯
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
//此處提示錯(cuò)誤,無法通過編譯,因?yàn)闊o法創(chuàng)建泛型數(shù)組
elements = new E[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
E result = elements[--size];
elements[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * DEFAULT_INITIAL_CAPACITY + 1);
}
}
}
消除泛型數(shù)組的方法有兩種:
1.直接繞過創(chuàng)建泛型數(shù)組,創(chuàng)建一個(gè)Object數(shù)組
//用法合法,但整體上而言不是類型安全的
elements = (E[])Object[DEFAULT_INITIAL_CAPACITY];
2.將域的類型從E[]改為Object[](推薦使用此種方法)
public class Stack<E> {
private Object[] elements;
...
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
E result = (E) elements[--size];
elements[size] = null;
return result;
}
}
使用泛型比使用需要在客戶端代碼中進(jìn)行轉(zhuǎn)換的類型來得更加安全,也更加容易。在設(shè)計(jì)新類型時(shí),要確保它們不需要這種轉(zhuǎn)換就可以使用,這通常意味著要把類做成是泛型的。
5.優(yōu)先考慮泛型方法
靜態(tài)工具方法通常比較適合泛型化。
編寫泛型方法與編寫泛型類相似,如下述代碼:
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
上述union方法并不是類型安全的,將其泛型化的代碼如下:
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
泛型后的union方法不僅適用性更強(qiáng),也是類型安全的,它確保了待合并集合的類型一致性,外部使用也無需進(jìn)行手工強(qiáng)制轉(zhuǎn)換。
泛型方法就像泛型一樣,使用起來比要求客戶端轉(zhuǎn)換輸入?yún)?shù)并返回值的方法來的更加安全,也更加容易。
6.利用有限制通配符來提升API的靈活性
參數(shù)類型是不可變的,對于任意兩個(gè)截然不同的類型Type1和Type2而言,List<Type1>既不是List<Type2>的子類型,也不是它的超類型。如List<String>并不是List<Object>的子類型,這似乎與直覺相悖,但實(shí)際上是有意義的,你可以將任何對象放進(jìn)一個(gè)List<Object>中,卻只能將字符串放進(jìn)List<String>中。
個(gè)人理解:泛型的參數(shù)類型是給編譯器使用的,供編譯器在獲取實(shí)參時(shí)進(jìn)行類型檢查,在返回結(jié)果時(shí)進(jìn)行類型轉(zhuǎn)換,在編譯完成后的字節(jié)碼中,泛型參數(shù)類型信息是被擦除了的。即是說,Java的泛型機(jī)制是在編譯階段實(shí)現(xiàn)的,編譯生成的字節(jié)碼在運(yùn)行期間并不包含泛型的類型信息。正是由于這種類型擦除機(jī)制,導(dǎo)致泛型并不具備協(xié)變性,才能保證泛型的類型安全。
泛型不具備協(xié)變性,但有時(shí),我們又需要使用協(xié)變帶來的靈活性,于是Java提供了有限制的通配符類型這種特殊的參數(shù)化類型:
GenericType<? extends E>:子類型通配符,通配符?表示E的某個(gè)子類型
GenericType<? super E>:超類型通配符,通配符?表示E的某個(gè)超類型
考慮Stack的公共API:
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
假如我們想增加一個(gè)方法,讓它按順序把一系列元素添加到Stack中,嘗試如下:
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
如果src中的元素類型與Stack的泛型參數(shù)類型完全匹配,是完全沒有問題的。但考慮這樣一種情形:有一個(gè)Stack<Number>,且調(diào)用了push(int val),從邏輯上講,下面的實(shí)現(xiàn)應(yīng)該是可以的:
Stack<Number> stack = new Stack<Number>();
Iterable<Integer> integers = ...;
stack.pushAll(integers);
實(shí)際情況是上述辦法并不可行,會(huì)導(dǎo)致編譯錯(cuò)誤。
顯然,我們的目的是想將E的某個(gè)子類型也放入Stack中,可以利用子類型通配符來做有限制的規(guī)定:
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
假設(shè)現(xiàn)在需要編寫一個(gè)popAll方法,使之與pushAll方法相呼應(yīng),popAll方法從Stack中彈出每個(gè)元素,并將這些元素添加到指定的集合中,嘗試如下:
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
如果dst的元素類型與Stack完全匹配,上述實(shí)現(xiàn)是沒有問題的。但考慮這樣一種情形:有一個(gè)Stack<Number>和Collection<Object>,從邏輯上講,下面的實(shí)現(xiàn)應(yīng)該是可以的:
Stack<Number> numStack = new Stack<Number>();
Collection<Object> coll = ...;
numStack.popAll(coll);
實(shí)際情況是上述辦法并不可行,會(huì)導(dǎo)致編譯錯(cuò)誤。Collection<Object>并不是Collection<Number>的超類型。
我們的目的是為了將類型為E的元素加入到目標(biāo)泛型集合中,且目標(biāo)集合的泛型參數(shù)類型只要是類型E的父類型即可,Java提供了父類型通配符來實(shí)現(xiàn)這種需求:
//此處的限定是:通配符類型是泛型參數(shù)類型的父類即可
public void popAll(Collection<? super E> dst) {
while(!isEmpty()) {
dst.add(pop());
}
}
為了獲得最大限度的靈活性,要在表示生產(chǎn)者或者消費(fèi)者的輸入?yún)?shù)上使用通配符類型。
為了便于記住要使用哪種通配符,引入下面的助記符:
PECS表示producer-extends,consumer-super。
如果參數(shù)化類型表示一個(gè)T生產(chǎn)者,就使用<? extends T>;如果它表示一個(gè)T消費(fèi)者,就使用<? super T>。在Stack示例中,pushAll的src參數(shù)產(chǎn)生E實(shí)例供Stack使用,因此src相應(yīng)的類型為Iterable<? extends E>;popAll的dst參數(shù)通過Stack消費(fèi)E實(shí)例,因此dst相應(yīng)的類型為Collection<? super E>。
PECS助記符突出了使用通配符類型的基本原則。
在API中使用通配符類型雖然比較需要技巧,但它可以使API變得靈活得多。
如果編寫的是將被廣泛使用的類庫,則一定要適當(dāng)?shù)乩猛ㄅ浞愋汀?br> 需要記住的基本原則是:producer-extends,comsumer-super(PECS)。
所有的comparable和comparator都是消費(fèi)者。
7.優(yōu)先考慮類型安全的異構(gòu)容器
泛型最常用于集合,如Set和Map,以及單元素的容器,如ThreadLocal和AtomicReference。在這些用法中,它都充當(dāng)了被參數(shù)化了的容器。這樣就限制了每個(gè)容器只能有固定數(shù)目的類型參數(shù)。一般來說,這種情況正是所想要的,一個(gè)Set只有一個(gè)類型參數(shù),表示它的元素類型;一個(gè)Map有兩個(gè)類型參數(shù),表示它的鍵和值類型。
但是,有時(shí)候你會(huì)需要更多的靈活性。例如,數(shù)據(jù)庫行可以有任意多列,如何才能以類型安全的方式訪問所有列哪?Java泛型提供了一種方法來解決這個(gè)問題:將鍵(key)進(jìn)行參數(shù)化而不是將容器參數(shù)化,然后將參數(shù)化的鍵提交給容器,來插入或者獲取值,用泛型系統(tǒng)來確保值的類型與它的鍵相符。
類Class在Java1.5中被泛化了,類的類型從字面上看不再只是簡單的Class,而是Class<T>,意味著String.class是屬于Class<String>類型,Integer.class屬于Class<Integer>類型。
當(dāng)一個(gè)類的字面文字被用在方法中,來傳達(dá)編譯時(shí)和運(yùn)行時(shí)的類型信息時(shí),被稱作type token。
假如需要設(shè)計(jì)一個(gè)Favorites類,它允許其客戶端從任意數(shù)量的其他類中,保存并獲得一個(gè)“最喜愛”的實(shí)例,代碼如下:
public class Favorites {
private Map<Class<?>, Object> favorities = new HashMap<Class<?>, Object>();
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null) {
throw new NullPointerException("Type is null");
}
favorities.put(type, instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorities.get(type));
}
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String fString = f.getFavorite(String.class);
int fInteger = f.getFavorite(Integer.class);
Class<?> fClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", fString, fInteger, fClass.getSimpleName());
}
}
//代碼打印結(jié)果為:Java cafebabe Favorites
Favorites實(shí)例是類型安全的:當(dāng)你向它請求String的時(shí)候,它不會(huì)返回一個(gè)Integer。同時(shí)它也是異構(gòu)的:不像普通的map,它的所有鍵都是不同類型的。
像Favorites這種類被稱為類型安全的異構(gòu)容器。
Favorites使用的類型令牌是無限制的,還可以利用有限制類型參數(shù)或有限制通配符來限制可以表示的類型:
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType = null;
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception e) {
throw new IllegalArgumentException();
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
集合API說明了泛型的常見用法,它限制每個(gè)容器只能有固定數(shù)目的類型參數(shù)。
可以將類型參數(shù)放在鍵上而不是容器上來避開這一限制。
對于這種類型安全的異構(gòu)容器,可以用Class對象作為鍵。
以這種方式使用的Class對象被稱作類型令牌。
也可以使用定制的鍵類型,例如,用一個(gè)DatabaseRow類型表示一個(gè)數(shù)據(jù)庫行(容器),用泛型Column<T>作為它的鍵。