程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 計算機程序的思維邏輯 (36),思維36

計算機程序的思維邏輯 (36),思維36

編輯:JAVA綜合教程

計算機程序的思維邏輯 (36),思維36


上節我們介紹了泛型的基本概念和原理,本節繼續討論泛型,主要討論泛型中的通配符概念。通配符有著令人費解和混淆的語法,但通配符大量應用於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編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved