本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結
上節我們介紹了泛型的基本概念和原理,本節繼續討論泛型,主要討論泛型中的萬用字元概念。萬用字元有著令人費解和混淆的語法,但萬用字元大量應用於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>
到底有什麼關係?
它們用的地方不一樣,我們解釋一下:
<T extends E>
用於定義型別引數,它宣告瞭一個型別引數T,可放在泛型類定義中類名後面、泛型方法返回值前面。<? extends E>
用於例項化型別引數,它用於例項化泛型變數中的型別引數,只是這個具體型別是未知的,只知道它是E或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;
}
複製程式碼
上面的程式碼就難以用萬用字元代替。
萬用字元還是型別引數?
現在我們再來看,泛型方法,到底應該用萬用字元的形式,還是加型別引數?兩者到底有什麼關係?我們總結下:
- 萬用字元形式都可以用型別引數的形式來替代,萬用字元能做的,用型別引數都能做。
- 萬用字元形式可以減少型別引數,形式上往往更為簡單,可讀性也更好,所以,能用萬用字元的就用萬用字元。
- 如果型別引數之間有依賴關係,或者返回值依賴型別引數,或者需要寫操作,則只能用型別引數。
- 萬用字元形式和型別引數往往配合使用,比如,上面的copy方法,定義必要的型別引數,使用萬用字元表達依賴,並接受更廣泛的資料型別。
超型別萬用字元
靈活寫入
還有一種萬用字元,與形式<? 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>
也比較容易混淆,我們再來比較下。
- 它們的目的都是為了使方法介面更為靈活,可以接受更為廣泛的型別。
<? super E>
用於靈活寫入或比較,使得物件可以寫入父型別的容器,使得父型別的比較方法可以應用於子類物件。<? extends E>
用於靈活讀取,使得方法可以讀取E或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>
,並分析了與型別引數形式的區別和聯絡。
簡單總結來說:
<?>
和<? extends E>
用於實現更為靈活的讀取,它們可以用型別引數的形式替代,但萬用字元形式更為簡潔。<? super E>
用於實現更為靈活的寫入和比較,不能被型別引數形式替代。
關於泛型,還有一些細節以及限制,讓我們下節來繼續探討。
未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。