1、基本概念和原理
之前我們一直強調(diào)數(shù)據(jù)類型的概念,Java有8中基本數(shù)據(jù)類型??梢远x類,類相當(dāng)于自定義數(shù)據(jù)類型,類之間還可以有組合和繼承。我們也介紹了接口,其中提到,很多時候我們關(guān)心的不是類型,而是能力,針對接口和能力編程,不僅可以復(fù)用代碼,還可以降低耦合度,提高靈活性。
泛型將接口的概念進一步延伸,泛型字面上的意思是廣泛的類型。類,接口和方法代碼可以應(yīng)用于非常廣泛的類型,代碼與他們能否操作的數(shù)據(jù)類型不再綁定在一起,同一套代碼可以應(yīng)用與多種數(shù)據(jù)類型,這樣,不僅可以復(fù)用代碼,降低耦合,而且可以提高代碼的可讀性和安全性。
-
1.1 一個簡單泛型類
-
基本概念
public class Pair<T> { T first; T second; public Pair(T first, T second){ this.first = first; this.second = second; } public T getFirst() { return first; } public T getSecond() { return second; } }T表示類型參數(shù),泛型就是類型參數(shù)化,處理的數(shù)據(jù)類型不再是固定的,而是可以作為參數(shù)傳遞。
public class Pair<U, V> { U first; V second; public Pair(U first, V second){ this.first = first; this.second = second; } public U getFirst() { return first; } public V getSecond() { return second; } }類型參數(shù)可以有多個。
-
基本原理
定義普通類,使用Object。
public class Pair { Object first; Object second; public Pair(Object first, Object second){ this.first = first; this.second = second; } public Object getFirst() { return first; } public Object getSecond() { return second; } } Pair minmax = new Pair(1,100); Integer min = (Integer)minmax.getFirst(); Integer max = (Integer)minmax.getSecond();實際上,Java泛型的內(nèi)部原理就是這樣的。
我們知道,Java有Java編譯器和Java虛擬機,編譯器將Java源代碼轉(zhuǎn)換成.class文件,虛擬機加載并運行.class文件。對于泛型類,Java編譯器會將泛型代碼轉(zhuǎn)換成普通的非泛型代碼,就像上面的Pair,將類型T擦掉,替換成Object,插入必要的強制類型轉(zhuǎn)換。Java虛擬機實際執(zhí)行的時候,它是不知道泛型這回事,只知道普通的類及代碼。
再強調(diào)一下,Java泛型是通過擦除實現(xiàn)的,類定義的類型參數(shù)T會被替換成Object。認識這一點是非常重要的,它有助于我們理解Java泛型的很多限制。 -
泛型的好處
- 更好的安全性
- 更好的閱讀性
語言和程序設(shè)計的一個重要目標是將bug盡量消滅在搖籃里,能消滅在寫代碼的時候,就不要等到代碼寫完程序運行的時候。只使用Object,代碼寫錯的時候,開發(fā)和編譯器都不能幫我們發(fā)現(xiàn)問題,運行的時候拋出類型轉(zhuǎn)換異常ClassCastException。如果使用泛型,就不可能犯這個錯誤。
通過使用泛型,為程序多設(shè)置一道安全防護網(wǎng)。使用泛型,還可以省去繁瑣的強制類型轉(zhuǎn)換,再加上明確的類型信息,代碼的可讀性也會更好。
-
-
1.2 容器類
泛型類最常用的用途是作為容器類。所謂容器類,簡單來說,就是容納并管理多項數(shù)據(jù)的類。計算機技術(shù)有一門專業(yè)的課程,叫數(shù)據(jù)結(jié)構(gòu),專門討論管理數(shù)據(jù)的各種方式。
這些數(shù)據(jù)結(jié)構(gòu)在Java中的實現(xiàn)主要就是Java中的各種容器類,甚至Java泛型的引入主要也是為了更好的支持Java容器。
我們先來實現(xiàn)一個簡單的動態(tài)數(shù)組容器。public class DynamicArray<E> { private static final int DEFAULT_CAPACITY = 10; private int size; private Object[] elementData; public DynamicArray() { this.elementData = new Object[DEFAULT_CAPACITY]; } private void ensureCapacity(int minCapacity) { int oldCapacity = elementData.length; if(oldCapacity >= minCapacity){ return; } int newCapacity = oldCapacity * 2; if(newCapacity < minCapacity) newCapacity = minCapacity; elementData = Arrays.copyOf(elementData, newCapacity); } 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; } }使用容器時,如果不指定泛型,可以添加任何元素,但是如果指定了泛型,就只能添加指定類型元素。
通常我們在使用容器時,都會指定泛型。這樣放進去的元素都是固定類型,取出來的也是固定類型。
試想一下,假如不指定泛型,可以放入任何類型元素。取出來時不知道是什么類型,更不知道如何轉(zhuǎn)換。
-
1.3 泛型方法
除了泛型類,方法也可以是泛型,而且,一個方法的是不是泛型的,與它所在類是不是泛型的沒有關(guān)系。
public static <T> int indexOf(T[] arr, T elm){ for(int i=0; i<arr.length; i++){ if(arr[i].equals(elm)){ return i; } } return -1; } -
1.4 泛型接口
接口也可以是泛型的,例如Comparable和Comparator接口都是泛型的。
-
1.5 類型參數(shù)的限定
Java支持限定這個類型參數(shù)的一個上界,也就是說,類型參數(shù)必須為給定的上界類型或者其子類型,這個限定是通過extends關(guān)鍵字來表示的。這個上界可以是某個具體的類或者某個具體的接口,也可以是其他的類型參數(shù),我們逐個介紹其應(yīng)用。
-
上界為某個具體的類
public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> { public NumberPair(U first, V second) { super(first, second); } }指定邊界后,類型擦除時就不會轉(zhuǎn)換成Object了,而是轉(zhuǎn)換成它的邊界類型。
-
上界為某個接口
public static <T extends Comparable> 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; }max方法計算泛型數(shù)組元素的最大值。計算最大值需要元素之間進行比較,要求元素實現(xiàn)Comparable接口,所以給類型參數(shù)設(shè)置了一個上邊界Comparable ,T必須實現(xiàn)Comparable接口。
不過直接這樣編寫代碼,Java會有一個警告信息,因為Comparable是一個泛型接口,它也需要一個類型參數(shù),所以完整的聲明應(yīng)該是:public static <T extends Comparable<T>> T max(T[] arr){ }這種形式成為遞歸類型限制,可以這么解讀:T表示一種數(shù)據(jù)類型,必須實現(xiàn)Comparable接口,且必須可以與相同類型的元素進行比較。
-
上界為其他類型參數(shù)
Java支持一個類型參數(shù)以另一個類型參數(shù)作為上界。為什么需要這個呢?我們看個例子,給上面的DynamicArray添加一個addAll。
public void addAll(DynamicArray<E> c) { for(int i=0; i<c.size; i++){ add(c.get(i)); } }但是這么寫有一些局限性,我們看它的使用代碼
DynamicArray<Number> numbers = new DynamicArray<>(); DynamicArray<Integer> ints = new DynamicArray<>(); ints.add(100); ints.add(34); numbers.addAll(ints); //編譯錯誤numbers是Number類型的容器,ints是Integer類型的容器,我們希望將ints假如numbers中,因為Integer是Number的子類,應(yīng)該說這是一個合理的需求和操作。
但是Java會在numbers.addAll(ints)這行代碼上提示編譯錯誤:addAll需要類型為DynamicArray<Number>,而提供的是DynamicArray<Integer>,不適用。
Integer是Number的子類,怎么會不適用呢?
事實就是這樣,確實不適用,而且是很有道理的,假設(shè)適用,我們看下會發(fā)生什么。DynamicArray<Integer> ints = new DynamicArray<>(); DynamicArray<Number> numbers = ints; //假設(shè)合法 numbers.add(new Double(12.34));那最后一行就是合法的,這時,DynamicArray<Integer>就會出現(xiàn)Double類型的值,而這顯示破壞了Java泛型關(guān)于泛型安全的保證。
我們強調(diào)一下,雖然Integer是Number的子類,但是DynamicArray<Integer>并不是DynamicArray<Number>的子類,DynamicArray<Integer>對象也不能賦值給DynamicArray<Number>的變量,這一點初看上去是違反直覺的,但這是事實,必須要理解這一點。不過我們的需求是合理的,將Integer類型的容器添加到Number類型的容器沒有問題,這個問題可以通過類型限定為其他類型參數(shù)來解決:
public <T extends E> void addAll(DynamicArray<T> c) { for(int i=0; i<c.size; i++){ add(c.get(i)); } }E是DynamicArray的類型參數(shù),T是addAll的類型參數(shù),T的上界限定為E,這樣,下面得代碼就沒有問題。
DynamicArray<Number> numbers = new DynamicArray<>(); DynamicArray<Integer> ints = new DynamicArray<>(); ints.add(100); ints.add(34); numbers.addAll(ints)
-
-
1.6 通配符
-
1.6.1 更簡潔的類型參數(shù)限定
我們之前提到的例子,為了將Integer類型容器添加到Number類型容器,我們的類型參數(shù)使用了其他類型參數(shù)作為上界,我們提到這種寫法過于繁瑣,它可以替換成下面簡潔的通配符形式:
public void addAll(DynamicArray<? extends E> c) { for(int i=0; i<c.size; i++){ add(c.get(i)); } }?表示通配符,<? 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>。
那么問題來了,同樣是extends關(guān)鍵字,同樣應(yīng)用于泛型,<T extends E>和<? extends E>有什么區(qū)別?
它們用的地方不一樣:
1.<T extends E>用于定義類型參數(shù),它聲明了一個類型參數(shù)T,可以放在泛型類定義中類名后面、泛型方法返回值前面。
2.<? extends E>用于實例化類型參數(shù),它用于實例化泛型變量的類型參數(shù),只是具體這個類型是未知的,只知道它是E或者E的子類。
雖然它們不一樣,但兩種寫法可以達到相同的目標。
那么我們到底應(yīng)該用那種形式呢?我們先進一步了解通配符,然后再解釋。 -
1.6.2 理解通配符
-
無限定通配符
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; }這種無限定通配符形式也可以改為使用類型參數(shù):
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就無法確保類型安全性,所以干脆禁止。我們來看個例子,假如允許寫入會發(fā)生什么:
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關(guān)于類型安全的承諾。
大部分情況下,這種限制是好的,但是使得一些理應(yīng)正確的基本操作無法完成,比如交換兩個元素的位置,看如下代碼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); }這個代碼看上去應(yīng)該是正確的,但是Java會提示編譯錯誤,兩行set語句是不合法的。不過,借助帶類型參數(shù)的限定方法,這個問題可以解決:
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可以調(diào)用swapInternal,而帶類型參數(shù)的swapInternal可以寫入。Java容器中就有類型這樣的用法,公共的API是通配符形式,但內(nèi)部調(diào)用帶類型參數(shù)的方法。
如果類型參數(shù)之前有依賴關(guān)系,也只能用類型參數(shù)形式。
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)); } }不過上面的聲明可以使用通配符簡化,兩個參數(shù)合并為一個參數(shù)
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)); } }如果返回值依賴類型參數(shù),也只能使用類型參數(shù)。
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; } -
通配符與類型參數(shù)形式總結(jié):
- 通配符形式都可以用類型參數(shù)形式替代,通配符能做的,用類型參數(shù)都能做。
- 通配符形式可以減少類型參數(shù),形式上往往更為簡單,可讀性也更好,所以,能使用通配符的地方就使用通配符。
- 如果類型參數(shù)之前有依賴關(guān)系、或者返回值依賴類型參數(shù),或者需要寫操作,則只能使用類型參數(shù)。
-
-
1.6.3 超類型通配符
還有一種通配符,與形式<? extends E>正好相反,它的形式為<? super E>,稱為超類型通配符。表示E的某個父類型。它有什么作用呢?有了它,我們就可以更靈活的寫入了。
如果沒有這些寫法,寫入會有一些限制。來看個例子,我們給DynamicArray添加一個方法:public void copyTo(DynamicArray<E> dest){ for(int i=0; i<size; i++){ dest.add(get(i)); } }這個方法很簡單,將當(dāng)前容器中的元素添加到傳入的容器中。我們希望可以這么使用:
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容器,這種用法應(yīng)該合情合理的,但是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)); } }這樣就沒有問題了。
-
1.6.4 超類型通配符在Comparable、Comparator的使用
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; } } }Child代碼如下:
class Child extends Base { public Child(int sortOrder) { super(sortOrder); } }注意:Child沒有重新實現(xiàn)Comparable接口,因此child的比較規(guī)則跟Base的比較規(guī)則是一致的。我們希望可以使用前面的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方法的類型參數(shù)T推斷為Child,但類型T的要求是extends Comparable<T>,而Child并沒有實現(xiàn)Comparable<Child>,它實現(xiàn)的是Comparable<Base>。
但我們的需求是合理的,Java為了解決這個問題,修改max方法的聲明,使用超類型通配符,如下所示:public static <T extends Comparable<? super T>> T max(DynamicArray<T> arr)<? super Child>可以匹配Base,所以整體是匹配的。
-
1.6.5 超類型通配符與類型參數(shù)限定。
類型參數(shù)限定只有extends形式,沒有super形式。如果之前copyTo方法:
public void copyTo(DynamicArray<? super E> dest)如果類型參數(shù)限定支持super形式,則應(yīng)該是:
public <T super E> void copyTo(DynamicArray<T> dest)事實是,Java不支持這種語法。
前面我們說過,對于限定的通配符形式<? extends E>,可以用類型參數(shù)限定替代,但是對于上面的超類型通配符,則無法使用類型參數(shù)限定替代。 -
1.6.6 通配符總結(jié)
本文介紹了泛型中三種通配符形式<?>、<? extends E>、<? super E>,并分析了與類型參數(shù)形式的區(qū)別和聯(lián)系,他們比較容易混淆,我們總結(jié)比較如下:
- 它們的目的都是為了使方法、接口更靈活,可以接收更為廣泛的類型。
- <? super E>用于靈活寫入或比較,使得對象可以寫入父類型的容器,使得父類型的比較方法可以應(yīng)用于子類對象,它不能被類型參數(shù)形式替代。
- <?>、<? extends E>用于靈活讀取,使得方法可以讀取E或者E的任意子類型的容器對象,它們可以用類型參數(shù)形式替代,但通配符形式更為簡潔。通常情況下他們交叉使用,公共的API是通配符形式,但內(nèi)部調(diào)用帶類型參數(shù)的方法。
-
2、參考文獻
Java編程的邏輯 第8章 泛型