本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html
上節咱們介紹了泛型的基本概念和原理,本節繼續討論泛型,主要討論泛型中的通配符概念。通配符有着使人費解和混淆的語法,但通配符大量應用於Java容器類中,它究竟是什麼?本節,讓咱們逐步來解析。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>
。ide
<T extends E>
與<? extends E>
那麼問題來了,一樣是extends關鍵字,一樣應用於泛型,<T extends E>
和<? extends E>
到底有什麼關係?this
它們用的地方不同,咱們解釋一下:spa
<T extends E>
用於定義類型參數,它聲明瞭一個類型參數T,可放在泛型類定義中類名後面、泛型方法返回值前面。<? extends E>
用於實例化類型參數,它用於實例化泛型變量中的類型參數,只是這個具體類型是未知的,只知道它是E或E的某個子類型。雖然它們不同,但兩種寫法常常能夠達成相同目標,好比,前面例子中,下面兩種寫法均可以:3d
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>
也比較容易混淆,咱們再來比較下。
<? 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編程及計算機技術的本質。用心原創,保留全部版權。