Java泛型之通配符
使用通配符的原因:Java中的數組是協變的,但是泛型不支持協變。
數組的協變
首先了解下什么是數組的協變,看下面的例子:
Number[] nums = new Integer[10]; // OK
因為Integer是Number的子類,一個Integer對象也是一個Number對象,所以一個Integer的數組也是一個Number的數組,這就是數組的協變。
Java把數組設計成協變的,在一定程度上是有缺陷的。因為盡管把Integer[]賦值給Number[],Integer[]可以向上轉型為Number[],但是數據元素的實際類型是Integer,只能向數組中放入Integer或者Integer的子類。如果向數組中放入Number對象或者Number其他子類的對象,對于編譯器來說也是可以通過編譯的。但是運行時JVM能夠知道數組元素的實際類型是Integer,當其它對象加入數組是就會拋出異常(java.lang.ArrayStoreException)。
泛型的設計目的之一就是保證了類型安全,讓這種運行時期的錯誤在編譯期就能發現,所以泛型是不支持協變的。例如:
List<Number> nums = new ArrayList<Integer>(); // incompatible types
當確實需要建立這種向上轉型的類型關系的時候,就需要用到泛型的通配符特性了。例如:
List<? extends Number> nums = new ArrayList<Integer>(); // OK
無邊界通配符(Unbounded Wildcards)
語法:
class-name<?> var-name
例子:
public static void print(List<?> list) {
for (Object obj : list) {
System.out.println(o);
}
}
List<?> list和List list的區別:
- List<?> list是表示持有某種特定類型對象的List,但是不知道是哪種類型;List list是表示持有Object類型對象的List。
- List<?> list因為不知道持有的實際類型,所以不能add任何類型的對象,但是List list因為持有的是Object類型對象,所以可以add任何類型的對象。
注意:List<?> list可以add(null),因為null是任何引用數據類型都具有的元素。
Pair<?> 和 Pair 的區別:
- Pair<?>的 ? getFirst()方法,返回值只能賦值給一個Object對象,它的void setFirst(? )方法不能被調用,甚至不能用Object調用。
-
為什么要使用這樣脆弱的類型?它對于許多簡單的操作非常有用。例如 ,下面這個方法將用來測試一個 pair 是否包含一個 mill 引用,它不需要實際的類型。 public static boolean hasNulls (Pair<?> p) { return p.getFirstO = null | | p.getSecondO = null ; } 通過將 hasNulls 轉換成泛型方法,可以避免使用通配符類型: public static <T> boolean hasNulls (Pair<T> p) 但是,帶有通配符的版本可讀性更強。
上邊界限定的通配符(Upper Bounded Wildcards)
語法:
class-name<? extends superclass> var-name
例子:
public static double sum(List<? extends Number> list) {
double s = 0.0;
for (Number num : list) {
s += num.doubleValue();
}
return s;
}
List<? extends Number> list = new ArrayList<Integer>(); // OK
List<? extends Number> list = new ArrayList<Object>(); // error
特性:
- List<? extends Number> list表示某種特定類型(Number或者Number的子類)對象的List。跟無邊界通配符一樣,因為無法確定持有的實際類型,所以這個List也不能add除null外的任何類型的對象。
list.add(new Integer(1)); // error
list.add(null); // OK
- 從list中獲取對象是是可以的(比如get(0)),因為在這個List中,不管實際類型是什么,但肯定都能轉型為Number。
Number n = list.get(0); // OK
Integer i = list.get(0); // error
- 事實上,只要是形式參數有使用類型參數的方法,在使用無邊界或者上邊界限定的通配符的情況下,都不能調用。比如以java.util.ArrayList為例:
public E get(int index) // 可以調用
public int indexOf(Object o) // 可以調用
public boolean add(E e) // 不能調用
下邊界限定的通配符(Lower Bounded wildcards)
語法:
class-name<? super subclass> var-name
例子:
public static void writeTo(List<? super Integer> list) {
// ...
}
List<? super Number> list = new ArrayList<Number>(); // OK
List<? super Number> list = new ArrayList<Object>(); // OK
List<? super Number> list = new ArrayList<Integer>(); // error
特性:
- List<? super Integer> list表示某種特定類型(Integer或者Integer的父類)對象的List。可以確定這個List持有的對象類型肯定是Integer或者其父類,所以往list里面add一個Integer或者其子類的對象是安全的,因為Integer或者其子類的對象都可以向上轉型為Integer的父類對象。但是因為無法確定實際類型,所以往list里面add一個Integer的父類對象是不安全的。
list.add(new Integer(1)); // OK
list.add(new Object()); // error
- 當從List<? super Integer> list獲取具體的數據的時候,JVM在編譯的時候知道實際類型可以是任何Integer的父類,所以為了安全起見,要用一個最頂層的父類對象來指向取出的數據,這樣就可以避免發生強制類型轉換異常了。
Object obj = list.get(0); // OK
Integer i = list.get(0); // error
PECS原則(Producer Extends Consumer Super)
從上面上邊界限定的通配符和下邊界限定的通配符的特性,可以知道:
- 對于上邊界限定的通配符,無法向其中加入任何對象,但是可以從中正常取出對象。
- 對于下邊界限定的通配符,可以存入subclass對象或者subclass的子類對象,但是取出時只能用Object類型變量指向取出的對象。
簡而言之,上邊界限定(extends)的通配符適合于內容的獲取,而下邊界限定(super)的通配符更適合于內容的存入。所以就有了一個PECS原則來很好的解釋這兩種通配符的使用原則。
- 當一個數據結構作為producer對外提供數據的時候,應該只能取數據而不能存數據,所以適合使用上邊界限定(extends)的通配符。
- 當一個數據結構作為consumer獲取并存入數據的時候,應該只能存數據而不能取數據,所以適合使用下邊界限定(super)的通配符。
- 如果既需要取數據也需要存數據,就不適合使用泛型的通配符。
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));
}
}
浙公網安備 33010602011771號