????????參數化類型是不變的( invariant ) 。 換句話說,對于任何兩個截然不同的類型 Typel 和 Type2 而言, List<Type1?>既不是 List<Type 2 > 的子類型,也不是它的超類型 。雖然 L ist<String>不是 List<Object>的子類型,這與直覺相悖,但是實際上很有意義 。你可以將任何對象放進一個List<Object>中,卻只能將字符串放進 List<String>中 。由于 List<String>不能像 List<O句ect> 能做任何事情,它不是一個子類型 。
????????有時候,我們需要的靈活性要比不變類型所能提供的更多 。比如第 29 條中的堆樓 。 提醒一下,下面就是它的公共 API:
public class Stack<E> {public Stack();public void push(E e);public E pop();public boolean isEmpty();
}
????????假設我們想要增加一個方法,讓它按順序將一系列的元素全部放到堆枝中 。 第一次嘗試如下:
public void pushAll(Iterable<E> src) {for(E e : src)push(e);
}
????????這個方法編譯時正確無誤,但是并非盡如人意 。 如果 Iterable 的 src 元素類型與堆棧的完全匹配,就沒有問題 。 但是假如有一個 Stack<Number>,并且調用了 push (intVal),這里的工ntVal 就是 Integer 類型 。 這是可以的,因為 Integer 是 Number 的一個子類型 。 因此從邏輯上來說,下面這個方法應該可行 :
Stack <Number> numberStack = new Stack<>() ;
Iterable<Integer> integers = ...;
numberStack. pushAll(integers);
但是,如果嘗試這么做,就會得到下面的錯誤消息,因為參數化類型是不可變的:
????????幸運的是,有一種解決辦法 。Java 提供了一種特殊的參數化類型,稱作有限制的通配符類型(bounded wildcard type ),它可以處理類似的情況 。pushAll 的輸入參數類型不應該為“ E 的 Iterable 接口”,而應該為“ E 的某個子類型的 Iterable 接口”通配符類型Iterable<?extends E >正是這個意思 。 (使用關鍵字 ex ten也有些誤導 :回憶一下第29 條中的說法,確定了子類型( subtype )后,每個類型便都是自身的子類型,即使它沒有將自身擴展 。)我們修改一下 pushAll 來使用這個類型:
public void pushAll(Iterable<? extends E> src) {for(E e : src)push(e);
}
????????修改之后,不僅 Stack 可以正確無誤地編譯,沒有通過初始的 pushAll 聲明進行編譯的客戶端代碼也一樣可以 。 因為 Stack 及其客戶端正確無誤地進行了編譯,你就知道一切都是類型安全的了 。
????????現在假設想要編寫一個 pushAll 方法,使之與 popAll 方法相呼應 。popAll 方法從堆校中彈出每個元素,并將這些元素添加到指定的集合中 。 初次嘗試編寫的 popAll 方法可能像下面這樣 :
public void popAll(Col1ection<E> dst) {while (!isEmpty())dst.add(pop());
}
????????此外,如果目標集合的元素類型與堆棧的完全匹配,這段代碼編譯時還是會正確無誤,并且運行良好 。 但是,也并不意味著盡如人意 。 假設你有一個 Stack<Number >和 Object 類型的變量 。 如果從堆校中彈出 一個元素,并將它保存在該變量中,它的編譯和運行都不會出錯,那你為何不能也這么做呢?
Stack<Number> numberStack = new Stack<Number>() ;
Collection<Object> objects = ...;
numberStack.popAll(objects) ;
????????如果試著用上述 的 popAll 版本編譯這段客戶端代碼,就會得到一個非常類似于第一次用 pushAll 時所得到的錯誤:Collection<Object >不是 Collection<Number>的子類型 。 這一次通配符類型同樣提供了一種解決辦法 。popAll 的輸入參數類型不應該為“ E 的集合”,而應該為“ E 的某種超類的集合”(這里的超類是確定的,因此 E 是它自身的一個超類型)。 仍有一個通配符類型正符合此意:Collection<? super E > 。 讓我們修改 popAll 來使用它:
public void popAll(Collection<? super E> dst) {while (!isEmpty())dst.add(pop();
}
????????做了 這個變動之后,Stack 和客戶端代碼就都可以正確無誤地編譯了 。
????????結論很明顯:為了獲得最大限度的靈活性,要在表示生產者或者消費者的輸入參數上使用通配符類型 。 如果某個輸入參數既是生產者,又是消費者,那么通配符類型對你就沒有什么好處了:因為你需要的是嚴格的類型匹配,這是不用任何通配符而得到的 。
????????下面的助記符便于讓你記住要使用哪種通配符類型 :
????????PECS 表示 producer-extends,consumer-super 。
????????換句話說,如果參數化類型表示一個生產者 T ,就使用<? extends T >;如果它表示一個消 費者 T ,就使用 <? super T > 。 在我們的 Stack 示例中,pushAll 的 src 參數產生 E 實 例供 Stack 使用 ,因 此 src 相 應的類型為 Iterable<? extends E> ; popAll的 dst 參數通過 Stack 消費 E 實例,因此 dst 相應的類型為 Collection<? s uper E > 。PECS 這個助記符突 出了使用通配符類型的基本原則 。Naftalin 和 Wadler 稱之為 Get αnd Put Principle。
????????如果使用得當,通配符類型對于類的用戶來說幾乎是無形的 。 它們使方法能夠接受它們應該接受的參數,并拒絕那些應該拒絕的參數 。 如果類的 用 戶必須考慮通配符類型,類的API 或許就會出錯 。
????????一般來說, 如果類型參數只在方法聲明中出現一次,就可以用通配符取代它 。 如果是無限制的類型參數,就用無限制的通配符取代它;如果是有限制的類型參數,就用有限制的通配符取代它。
????????總而言之,在 API 中使用通配符類型雖然比較需要技巧,但是會使 API 變得靈活得多 。 如果編寫 的是將被廣泛使用的類庫, 則一定要適當地利用通配符類型 。 記住基本的原則:producer-extends,consumer-super(PECS ) 。 還要記住所有的 comparable 和comparator 都是消費者 。