Java泛型食用筆記(四) -- 通配符
1. 三種通配符
通配符為一個泛型類所指定的類型集合提供了一個有用的類型范圍,Java 里有三種通配符:
- 無限定通配符, <?>
- 上界限定符, <? extends Number>
- 下界限定符, <? super Number>
上界限定符接受 extends 后面類的本身與其子類, 下界限定符接受 super 后面類的本身與其父類。無限定通配符接受任何類。
2. 無限定通配符
無限定通配符表示匹配任意類。ArrayList<?> 和 ArrayList,ArrayList<Object> 看上去功能有點類似,但實際卻不一樣。 ArrayList<?> 是任意 ArrayList<T> 的超類,而我們知道 ArrayList<Object> 并不是。ArrayList<?> 雖然可以匹配任何類,我們并不知道那個類的類型,但我們知道里面的所有元素都有相同的類。而原始類 ArrayList 可以添加任意不同類型的元素,編譯器并不能進行類型判斷,但運行的時候可能會拋出異常。ArrayList<Object> 明確的告訴我們可以添加任何類型的對象。
看一個無限定通配符例子
public class GenericTest07 {
public static void printCollection(Collection<Object> col1) {
for (Object obj : col1) {
System.out.println(obj);
}
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
printCollection(list); // compile error
}
}
由于 ArrayList<Integer> 不是 Collection<Object> 的子類,故編譯不能通過。改用無限定通配符即可編譯通過:
public class GenericTest07 {
public static void printCollection(Collection<?> col1) {
for (Object obj : col1) {
System.out.println(obj);
}
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
printCollection(list);
}
}
Collection<?> 是任意 T ArrayList<T> 的超類,可以編譯通過。但是,在printCollection 方法中,卻不能使用任何帶有類型參數(shù)的方法,比如 add(T t), 而 remove(Object obj) contains(Object obj) 等方法都是可以調(diào)用的。
再看一個例子:
public class GenericTest08 {
public static void reBox(Box<?> box) {
System.out.println(box.put(box.get()));
}
}
public interface Box<T> {
public T get();
public void put(T t);
}
這段代碼初看應(yīng)該是 work 的,然而事實可能要讓你失望了,編譯會提示類型不兼容的錯誤。事實上,你根本就不能在此處調(diào)用 box.put() 方法,因為box.put() 的形參類型是未知的,編譯器不能檢驗你的實參類型是否與形參類型一致。
有沒有辦法實現(xiàn)這個邏輯的,我們需要借助一個輔助方法。
public class GenericTest08 {
public static void reBox(Box<?> box) {
reboxHelper(box);
}
private static <V> void reboxHelper(Box<V> box) {
box.put(box.get());
}
}
reboxHelper 方法幫助編譯器保留一部分類型信息。
上界限定符
我們知道,和數(shù)組不一樣,泛型并不是協(xié)變的。比如 Dog 是 Animal 的子類,那 Dog[] 也是 Animal[] 的子類,但 List<Dog> 并不是 List<Animal> 的子類。這個時候上界限定符的作用就體現(xiàn)出來了,寫法是 List<? extends Animal>,可以解釋為“可以放入任何 Animal 及其子類的列表”,之前講到泛型編譯后會抹除類型參數(shù)成 Object 類型,當你使用上界限定符后,就抹除成上界。
看代碼
public class GenericTest {
public static void main(String[] args) {
ArrayList<Integer> holder = new ArrayList<Integer>();
holder.add(new Integer(1));
// ArrayList<Number> numHolder = holder; // 1. compile failed. ArrayList<Number> 不是 ArrayList<Integer> 的父類
ArrayList<? extends Number> numHolder = holder; // 2. ok
Number num = numHolder.get(0); // 3. ok. return Number
numHolder.contains(new Integer(2)); // 4. ok
// numHolder.add(new Integer(2)); // 5. compile error
}
}
1 處編譯錯誤是因為泛型不是協(xié)變;Integer 是 Number 子類,故 2 處正確;3 處正確,正如上段所說,上界限定符抹除成上屆,返回類型為 Number;4 處 ok,是因為 contains 方法接受的是 Object 類。5 處需要稍微注意,add 的形參類型也是 <? extends Number>,編譯器只能知道類型是 Number 的子類,并不能確定具體類型是什么,因此無法驗證類型的安全性。
下界限定符
下界限定符也稱為超類通配符,寫法是 List<? super Dog>, 列表可以接受 Dog 類型及其父類對象。
上一接的第 5 處,如果向往里添加元素,需要使用下界限定符。
public void writeTo(List<? super Integer> list) {
list.add(new Integer(2)); // ok
}
<? super Integer> 說明類型參數(shù)一定是 Integer 的父類。因此向里添加 Integer 類或其子類的對象一定是安全的。
PECS 原則
根據(jù)上面的例子,我們看到,使用上界限定符定義的類,可以向外提供東西,也就是說作為 Producer。使用下界限定符定義的類,可以作為 Consumer 接收外部往自身添加東西。
總結(jié)起來就是, "Producer Extends, Consumer Super":
- "Producer Extends" - 如果你需要一個只讀類型,用它來produce T,那么使用
<? extends T> - "Consumer Super" - 如果你需要一個只寫類型,用它來consume T,那么使用
<? super T> - 如果需要同時讀取以及寫入,那么我們就不能使用通配符了
下面一個方法同時涉及了這兩條規(guī)則。
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++)
dest.set(i, src.get(i));
}
}