上節我們介紹了泛型的基本概念和原理,本節繼續討論泛型,主要討論泛型中的通配符概念。通配符有著令人費解和混淆的語法,但通配符大量應用於Java容器類中,它到底是什麼?本節,讓我們逐步來解析。
更簡潔的參數類型限定
在上節最後,我們提到一個例子,為了將Integer對象添加到Number容器中,我們的類型參數使用了其他類型參數作為上界,代碼是:
public <T extends E> void addAll(DynamicArray<T> c) { for(int i=0; i<c.size; i++){ add(c.get(i)); } }
我們提到,這個寫法有點啰嗦,它可以替換為更為簡潔的通配符形式:
public void addAll(DynamicArray<? extends E> c) { for(int i=0; i<c.size; i++){ add(c.get(i)); } }
這個方法沒有定義類型參數,c的類型是DynamicArray<? extends E>,?表示通配符,<? extends E>表示有限定通配符,匹配E或E的某個子類型,具體什麼子類型,我們不知道。
使用這個方法的代碼不需要做任何改動,還可以是:
DynamicArray<Number> numbers = new DynamicArray<>(); DynamicArray<Integer> ints = new DynamicArray<>(); ints.add(100); ints.add(34); numbers.addAll(ints);
這裡,E是Number類型,DynamicArray<? extends E>可以匹配DynamicArray<Integer>。
<T extends E>與<? extends E>
那麼問題來了,同樣是extends關鍵字,同樣應用於泛型,<T extends E>和<? extends E>到底有什麼關系?
它們用的地方不一樣,我們解釋一下:
雖然它們不一樣,但兩種寫法經常可以達成相同目標,比如,前面例子中,下面兩種寫法都可以:
public void addAll(DynamicArray<? extends E> c) public <T extends E> void addAll(DynamicArray<T> c)
那,到底應該用哪種形式呢?我們先進一步理解通配符,然後再解釋。
理解通配符
無限定通配符
還有一種通配符,形如DynamicArray<?>,稱之為無限定通配符,我們來看個使用的例子,在DynamicArray中查找指定元素,代碼如下:
public static int indexOf(DynamicArray<?> arr, Object elm){ for(int i=0; i<arr.size(); i++){ if(arr.get(i).equals(elm)){ return i; } } return -1; }
其實,這種無限定通配符形式,也可以改為使用類型參數。也就是說,下面寫法:
public static int indexOf(DynamicArray<?> arr, Object elm)
可以改為:
public static <T> int indexOf(DynamicArray<T> arr, Object elm)
不過,通配符形式更為簡潔。
通配符的只讀性
通配符形式更為簡潔,但上面兩種通配符都有一個重要的限制,只能讀,不能寫。
怎麼理解呢?看下面例子:
DynamicArray<Integer> ints = new DynamicArray<>(); DynamicArray<? extends Number> numbers = ints; Integer a = 200; numbers.add(a); numbers.add((Number)a); numbers.add((Object)a);
三種add方法都是非法的,無論是Integer,還是Number或Object,編譯器都會報錯。為什麼呢?
?就是表示類型安全無知,? extends Number表示是Number的某個子類型,但不知道具體子類型,如果允許寫入,Java就無法確保類型安全性,所以干脆禁止。我們來看個例子,看看如果允許寫入會發生什麼:
DynamicArray<Integer> ints = new DynamicArray<>(); DynamicArray<? extends Number> numbers = ints; Number n = new Double(23.0); Object o = new String("hello world"); numbers.add(n); numbers.add(o);
如果允許寫入Object或Number類型,則最後兩行編譯就是正確的,也就是說,Java將允許把Double或String對象放入Integer容器,這顯然就違背了Java關於類型安全的承諾。
大部分情況下,這種限制是好的,但這使得一些理應正確的基本操作都無法完成,比如交換兩個元素的位置,看代碼:
public static void swap(DynamicArray<?> arr, int i, int j){ Object tmp = arr.get(i); arr.set(i, arr.get(j)); arr.set(j, tmp); }
這個代碼看上去應該是正確的,但Java會提示編譯錯誤,兩行set語句都是非法的。不過,借助帶類型參數的泛型方法,這個問題可以這樣解決:
private static <T> void swapInternal(DynamicArray<T> arr, int i, int j){ T tmp = arr.get(i); arr.set(i, arr.get(j)); arr.set(j, tmp); } public static void swap(DynamicArray<?> arr, int i, int j){ swapInternal(arr, i, j); }
swap可以調用swapInternal,而帶類型參數的swapInternal可以寫入。Java容器類中就有類似這樣的用法,公共的API是通配符形式,形式更簡單,但內部調用帶類型參數的方法。
參數類型間的依賴關系
除了這種需要寫的場合,如果參數類型之間有依賴關系,也只能用類型參數,比如說,看下面代碼,將src容器中的內容拷貝到dest中:
public static <D,S extends D> void copy(DynamicArray<D> dest, DynamicArray<S> src){ for(int i=0; i<src.size(); i++){ dest.add(src.get(i)); } }
S和D有依賴關系,要麼相同,要麼S是D的子類,否則類型不兼容,有編譯錯誤。不過,上面的聲明可以使用通配符簡化一下,兩個參數可以簡化為一個,如下所示:
public static <D> void copy(DynamicArray<D> dest, DynamicArray<? extends D> src){ for(int i=0; i<src.size(); i++){ dest.add(src.get(i)); } }
通配符與返回值
還有,如果返回值依賴於類型參數,也不能用通配符,比如,計算動態數組中的最大值,如下所示:
public static <T extends Comparable<T>> T max(DynamicArray<T> arr){ T max = arr.get(0); for(int i=1; i<arr.size(); i++){ if(arr.get(i).compareTo(max)>0){ max = arr.get(i); } } return max; }
上面的代碼就難以用通配符代替。
通配符還是類型參數?
現在我們再來看,泛型方法,到底應該用通配符的形式,還是加類型參數?兩者到底有什麼關系?我們總結下:
超類型通配符
靈活寫入
還有一種通配符,與形式<? extends E>正好相反,它的形式為<? super E>,稱之為超類型通配符,表示E的某個父類型,它有什麼用呢?有了它,我們就可以更靈活的寫入了。
如果沒有這種語法,寫入會有一些限制,來看個例子,我們給DynamicArray添加一個方法:
public void copyTo(DynamicArray<E> dest){ for(int i=0; i<size; i++){ dest.add(get(i)); } }
這個方法也很簡單,將當前容器中的元素添加到傳入的目標容器中。我們可能希望這麼使用:
DynamicArray<Integer> ints = new DynamicArray<Integer>(); ints.add(100); ints.add(34); DynamicArray<Number> numbers = new DynamicArray<Number>(); ints.copyTo(numbers);
Integer是Number的子類,將Integer對象拷貝入Number容器,這種用法應該是合情合理的,但Java會提示編譯錯誤,理由我們之前也說過了,期望的參數類型是DynamicArray<Integer>,DynamicArray<Number>並不適用。
如之前所說,一般而言,不能將DynamicArray<Integer>看做DynamicArray<Number>,但我們這裡的用法是沒有問題的,Java解決這個問題的方法就是超類型通配符,可以將copyTo代碼改為:
public void copyTo(DynamicArray<? super E> dest){ for(int i=0; i<size; i++){ dest.add(get(i)); } }
這樣,就沒有問題了。
靈活比較
超類型通配符另一個常用的場合是Comparable/Comparator接口。同樣,我們先來看下,如果不使用,會有什麼限制。以前面計算最大值的方法為例,它的方法聲明是:
public static <T extends Comparable<T>> T max(DynamicArray<T> arr)
這個聲明有什麼限制呢?我們舉個簡單的例子,有兩個類Base和Child,Base的代碼是:
class Base implements Comparable<Base>{ private int sortOrder; public Base(int sortOrder) { this.sortOrder = sortOrder; } @Override public int compareTo(Base o) { if(sortOrder < o.sortOrder){ return -1; }else if(sortOrder > o.sortOrder){ return 1; }else{ return 0; } } }
Base代碼很簡單,實現了Comparable接口,根據實例變量sortOrder進行比較。Child代碼是:
class Child extends Base { public Child(int sortOrder) { super(sortOrder); } }
這裡,Child非常簡單,只是繼承了Base。注意,Child沒有重新實現Comparable接口,因為Child的比較規則和Base是一樣的。我們可能希望使用前面的max方法操作Child容器,如下所示:
DynamicArray<Child> childs = new DynamicArray<Child>(); childs.add(new Child(20)); childs.add(new Child(80)); Child maxChild = max(childs);
遺憾的是,Java會提示編譯錯誤,類型不匹配。為什麼不匹配呢?我們可能會認為,Java會將max方法的類型參數T推斷為Child類型,但類型T的要求是extends Comparable<T>,而Child並沒有實現Comparable<Child>,它實現的是Comparable<Base>。
但我們的需求是合理的,Base類的代碼已經有了關於比較所需要的全部數據,它應該可以用於比較Child對象。解決這個問題的方法,就是修改max的方法聲明,使用超類型通配符,如下所示:
public static <T extends Comparable<? super T>> T max(DynamicArray<T> arr)
就這麼修改一下,就可以了,這種寫法比較抽象,將T替換為Child,就是:
Child extends Comparable<? super Child>
<? super Child>可以匹配Base,所以整體就是匹配的。
沒有<T super E>
我們比較一下類型參數限定與超類型通配符,類型參數限定只有extends形式,沒有super形式,比如說,前面的copyTo方法,它的通配符形式的聲明為:
public void copyTo(DynamicArray<? super E> dest)
如果類型參數限定支持super形式,則應該是:
public <T super E> void copyTo(DynamicArray<T> dest)
事實是,Java並不支持這種語法。
前面我們說過,對於有限定的通配符形式<? extends E>,可以用類型參數限定替代,但是對於類似上面的超類型通配符,則無法用類型參數替代。
通配符比較
兩種通配符形式<? super E>和<? extends E>也比較容易混淆,我們再來比較下。
Java容器類的實現中,有很多這種用法,比如說,Collections中就有如下一些方法:
public static <T extends Comparable<? super T>> void sort(List<T> list) public static <T> void sort(List<T> list, Comparator<? super T> c) public static <T> void copy(List<? super T> dest, List<? extends T> src) public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)
通過上節和本節,我們應該可以理解這些方法聲明的含義了。
小結
本節介紹了泛型中的三種通配符形式,<?>、<? extends E>和<? super E>,並分析了與類型參數形式的區別和聯系。
簡單總結來說:
關於泛型,還有一些細節以及限制,讓我們下節來繼續探討。
----------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。