效率程式設計 之「泛型」
溫馨提示:本系列博文(含示例程式碼)已經同步到 GitHub,地址為「java-skills」,歡迎感興趣的童鞋
Star
、Fork
,糾錯。
第 1 條:請不要在新程式碼中使用原生態型別
每種泛型都定義了一組引數化型別,其構成格式為:先是類或者介面的名稱,接著用尖括號(<>
)把對應於泛型形式型別引數的實際型別引數列表括起來。例如,List<String>
(讀作“字串列表”)是一個引數化的型別,表示元素型別為String
的列表。每個泛型都定義了一個原生態型別,即不帶任何實際引數的泛型名稱。例如,與List<E>
相對應的原生態型別是List
。原生態型別就像是從型別宣告中刪除了所有泛型資訊一樣。實際上,原生態型別List
List
完全一樣。但是,如果使用原生態型別,就失掉了泛型在安全性和表述性方面的所有優勢。
泛型型別有子類化的規則,List<String>
是原生態型別List
的一個子型別,而不是引數化型別List<Object>
的子型別。因此,如果使用像List
這樣的原生態型別,就會失掉型別安全性,但是如果使用像List<Object>
這樣的引數化型別,則不會。如果要使用泛型,但不確定或者不關心實際的型別引數,就可以使用一個問號代替,稱之為“無限制的萬用字元型別”。此外,在類文字中必須使用原生態型別(但是允許使用陣列型別和基本型別),而不允許使用引數化型別。換句話說,List.class
String[].class
和int.class
都合法,但是List<String>.class
和List<?>.class
則不合法。
由於泛型資訊可以在執行時被擦除,因此在引數化型別而非萬用字元型別上使用instanceof
操作符是非法的。總之,使用原生態型別會在執行時導致異常,因此不要在新程式碼中使用。原生態型別只是為了與引入泛型之前的遺留程式碼進行相容和互用而提供的。
第 2 條:消除非受檢警告以及列表優先於陣列
要儘可能地消除每一個非受檢警告。如果無法消除警告,同時可以證明引起警告的程式碼是型別安全的,只有在這種情況下,可以用一個@SuppressWarnings("unchecked")
SuppressWarnings
註解。此外,將SuppressWarnings
註解放在return
語句中是非法的,因為它不是一個宣告,而是應該宣告一個區域性變數來保持返回值,並註解其宣告。每當使用@SuppressWarnings("unchecked")
註解時,都要新增一條註釋,說明為什麼這麼做事安全的。
陣列和泛型不能很好地混合使用,因為陣列是協變的、具體的,而泛型只在編譯時強化它們的型別資訊並在執行時擦除它們的元素型別資訊。因此,建立泛型、引數化或者型別引數的陣列是非法的。但是,建立泛型、引數化或者型別引數的列表卻是合法的。為了獲得泛型帶來的型別安全,在面對陣列和列表都能解決的問題時,要優先選擇列表。
第 3 條:利用有限制萬用字元來提示 API 的靈活性
引數化型別是不可變的。換句話說,對於任何兩個截然不同的型別Type1
和Type2
而言,List<Type1>
既不是List<Type2>
的子型別,也不是它的超型別。考慮下面的堆疊 API:
public class SimpleStackPECS {
public SimpleStackPECS();
public void push(E e);
public E pop();
public boolean isEmpty();
}
假設我們想要增加一個方法,讓它按順序將一系列的元素全部放到堆疊中。這是第一次嘗試,如下:
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
上面的方法編譯時正確無誤,結果卻不盡人意。如果Iterable
的元素型別與堆疊的完成匹配,沒有問題;但是如果有一個SimpleStackPECS<Number>
,並且呼叫了push(intVal)
,這裡intVal
為Integer
型別。這是可以的,因為Integer
是Number
的一個子型別。因此從邏輯上來說,下面的程式碼應該是可以的:
SimpleStackPECS<Number> simpleStack = new SimpleStackPECS<Number>();
Iterable<Integer> integers = ...;
simpleStack.pushAll(integers);
但是,如果嘗試這麼做,就會得到下面的錯誤訊息,因為如前文所述,引數化型別是不可變的:
幸運的是,有一種解決辦法。Java 提供了一種特殊的引數化型別,稱之為“有限制的萬用字元型別”,來處理類似的情況。pushAll()
的輸入引數不應該為“E
的Iterable
介面”,而應該為“E
的某個子型別的Iterable
介面”,有一種萬用字元型別正符合此意:Iterable<? extends E>
。接下來,我們修改一下pushAll()
來使用這個型別:
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
這麼修改了之後,上面我們遇到的問題都解決啦!與pushAll()
相對應的,我們提供一個popAll()
方法,從堆疊中彈出每個元素,並將這些元素新增到指定的集合中。初始嘗試編寫的popAll()
方法可能像下面這樣:
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
st.add(pop());
}
}
如果目標集合的元素型別與堆疊的元素型別完全相同,這段程式碼編譯時還是會正確無誤,執行得很好。現在假設我們有一個SimpleStackPECS<Number>
和型別為Object
的變數,如果從堆疊中彈出每一個元素,並將它儲存到該變數中:
SimpleStackPECS<Number> simpleStack = new SimpleStackPECS<Number>();
Collection<Object> objects = ...;
simpleStack.popAll(objects);
我們將會得到一個非常類似於第一次呼叫pushAll()
時所得到的錯誤:
這一次,萬用字元型別同樣提供了一種解決辦法。popAll()
方法的輸入引數型別不應該為“E
的集合”,而應該為“E
的某個超型別的集合”,有一種萬用字元型別正符合此意:Collection<? supper E>
。接下來,我們修改一下popAll()
來使用這個型別:
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
做了這個變動之後,SimpleStackPECS
和客戶端的程式碼都可以正確無誤地編譯了。結論很明顯,為了獲得最大限度的靈活性,要在表示生產者或消費者的輸入引數上使用萬用字元型別。如果某個輸入引數既是生產者,又是消費者,那麼萬用字元型別對你就沒有什麼好處了:因為你需要的是嚴格的型別匹配,這是不用任何萬用字元而得到的。下面的助記符便於我們記住要使用哪種萬用字元型別:
PECS
,表示producer-extend
,consumer-super
。
換句話說,如果引數化型別表示一個T
生產者,就使用<? extend T>
;如果引數化型別表示一個T
消費者,就使用<? super T>
。而且,Comparable
始終是消費者,因此使用時始終應該是Comparable<? super T>
優先於Comparable<T>
;對於Comparator
也是一樣,因此使用時始終應該是Comparator<? super T>
優先於Comparator<T>
。一般來說,如果引數型別只在方法宣告中出現一次,就可以用萬用字元取代它。
———— ☆☆☆ —— 返回 -> 那些年,關於 Java 的那些事兒 <- 目錄 —— ☆☆☆ ————