類型參數的限定
無論是泛型類、泛型方法還是泛型接口,關於類型參數,我們都知之甚少,只能把它當作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);
}