【Java編程的邏輯】 泛型 & 參數限定 & 通配符

類型參數的限定

無論是泛型類、泛型方法還是泛型接口,關於類型參數,我們都知之甚少,只能把它當作Object,但Java支持限定這個參數的一個上界,也就是說:參數必須爲給定的上界類型或其子類型,這個限定是通過extends關鍵字來表示的。

上界爲某個具體類

public class NumberPair<U extends Number> {

}

指定邊界之後,類型擦除時就不會轉換爲Object了,而是會轉換爲它的邊界類型。

上界爲某個接口

在泛型方法中,我們想要參數必須實現了某個接口。

public static <T extends Comparable<T>> T max(T[] arr) {
    T max = arr[0];
    for(int i = 1; i < arr.length; i++) {
        if(arr[i].compareTo(max) > 0) {
            max = arr[i];
        }
    }
    return max;
}

<T extends Comparable<T>>可能會令人比較費解,這稱爲遞歸類型限制。 T表示一種數據類型,必須實現Comparable接口,且必須可以和同類型的元素進行比較。

上界爲其他類型參數

假設我們有這麼一個類,該類主要仿寫集合中的一些功能。

public class DynamicArray<E> { 
    private Object[] elementData;
    private static final int DEFAULT_CAPACITY = 10;
    private int size;
    public DynamicArray() {
        this.elementData = new Object[DEFAULT_CAPACITY];
    }

    public void add(E e) {
        // 擴容方法,在此省略
        ensureCapacity(size + 1);
        elementData[size++] = e;
    }
    public E get(int index) {
        return (E) elementData[index];
    }

    public int size() {
        return size;
    }

    public E set(int index, E element) {
        E oldValue = get(index);
        elementData[index] = element;
        return oldValue;
    }
}

現在我們想添加一個功能,就是一次插入一個DynamicArray。我們可能會想當然的這麼想

public void addAll(DynamicArray<E> c) {
    for(int i = 0; i < c.size; i++) {
        add(c.get(i));
    }
}

測試:

public static void main(String[] args) {
    DynamicArray<Number> numbers = new DynamicArray<>();
    DynamicArray<Integer> ints = new DynamicArray<>();
    numbers.add(99);
    ints.add(100);
    numbers.addAll(ints);
}

這個時候,其實最後一行的addAll方法是會報錯的。
numbers是一個Number類型的容器,ints是一個Integer類型的容器,Integer是Number的子類,我們希望把ints添加到numbers中,應該說是比較合理的。

但是我們這樣的addAll方法是有問題的,我們可以這樣來假設,假設剛纔的addAll方法是可以編譯成功的

DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<Number> numbers = ints;    // 假設合法
numbers.add(new Double(3.14));          

這樣是不是就發現問題了?破壞了Java泛型關於類型安全的保證

這裏的原因是:Integer是Number的子類,但是DynamicArray<Integer>不是DynamicArray<Number>的子類。

但是這裏的需求是沒有問題的,將Integer添加到Number容器中。我們可以通過類型限定來解決:

public <T extends E> void addAll(DynamicArray<T> c) {
    for(int i = 0; i < c.size; i++) {
        add(c.get(i));
    }
}

通配符

在上面的那個示例中,addAll方法,我們使用了其他類型參數作爲上界。但是這種寫法有點繁瑣,我們可以通過通配符寫出更簡潔的形式。

public void addAll(DynamicArray<? extends E> c) {
    for(int i = 0; i < c.size; i++) {
        add(c.get(i));
    }
}

?表示通配符,<? extends E>表示有限定通配符
那麼問題就來了<T extedns E><? extends E>有什麼關係區別呢?

  • <T extedns E> 用於定義類型參數,它聲明瞭一個類型參數T,可以房子泛型類定義中類名後面、泛型方法返回值前面
  • <? extends E>用於實例化類型參數,它用於實例化泛型變量中的類型參數,只是這個具體類型是未知的,只知道它是E或E的某個子類。

理解通配符

上面提到了有限定通配符,相應的還有無限定通配符,這裏我們繼續來擴展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 <T> int indexOf(DynamicArray<T> arr, Object elm) {
    for(int i = 0; i < arr.size(); i++) { 
        if(arr.get(i).equals(elm)) {
            return i;
        }
    }
    return -1;
}

上面兩種方法可以達到同樣的作用,不過通配符形式更爲簡潔。 雖然通配符形式更爲簡潔,但上面兩種通配符都有一個重要的限制:只能讀,不能寫

public static void main(String[] args) {
    DynamicArray<Integer> ints = new DynamicArray<>();
    DynamicArray<? extends Number> numbers = ints;
    Integer a = 998;
    ints.add(a);
    numbers.add(a);
    numbers.add((Number)a);
    numbers.add((Object)a);
}

後面numbers.add()三種方式都是非法的。 因爲<? extends Number>表示的是Number的某個子類,但是不知道具體子類型,如果允許寫入,Java就無法確保類型安全了,所以這樣的寫法就是非法的了。

既然無法寫入,那麼這種形式的實際意義是什麼呢?
我們可以將公共API的方法使用通配符形式,但內部調用帶類型參數的方法

private static <T> void swapInternal(DynamicArray<T> arr, int i, int j) {
    T temp = arr.get(i);
    arr.set(i, arr.get(j));
    arr.set(j, temp);
}

public static void swap(DynamicArray<?> arr, int i, int j) {
    swap(arr, i, j);
}
發佈了98 篇原創文章 · 獲贊 97 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章