在Java SE1.5中,增加了一個新的特性:泛型(日本語中的總稱型)。何謂泛型呢?通俗的說,就是泛泛的指定對象所操作的類型,而不像常規方式一樣使用某種固定的類型去指定。泛型的本質就是將所操作的數據類型參數化,也就是說,該數據類型被指定爲一個參數。這種參數類型可以使用在類、接口以及方法定義中。
一、爲什麼使用泛型呢?
在以往的J2SE中,沒有泛型的情況下,通常是使用Object類型來進行多種類型數據的操作。這個時候操作最多的就是針對該Object進行數據的強制轉換,而這種轉換是基於開發者對該數據類型明確的情況下進行的(比如將Object型轉換爲String型)。倘若類型不一致,編譯器在編譯過程中不會報錯,但在運行時會出錯。
使用泛型的好處在於,它在編譯的時候進行類型安全檢查,並且在運行時所有的轉換都是強制的,隱式的,大大提高了代碼的重用率。
二、泛型的簡單例子:
首先,我們來看看下面兩個普通的class定義
private String myStr;
public String getStr() {
return myStr;
}
public void setStr(str) {
myStr = str;
}
}
public class getDouble {
private Double myDou;
public Double getDou() {
return myDou;
}
public void setDou(dou) {
myDou = dou;
}
}
這兩個class除了所操作的數據類型不一致,其他機能都是相同的。現在,我們可以使用泛型來將上面兩個class合併爲一個,從而提高代碼利用率,減少代碼量。
public class getObj<T> {
private T myObj ;
public T getObj() {
return myObj;
}
public void setObj<T obj> {
myObj = obj;
}
}
那麼,使用了泛型後,如何生成這個class的實例來進行操作呢?請看下面的代碼:
getObj<String> strObj = new getObj<String>();
strObj.setObj(“Hello Nissay”);
System.out.println(strObj.getObj());
getObj<Double> douObj = new getObj<Double>();
douObj.setObj(new Double(“116023”));
System.out.println(douObj.getObj());
三、例子分析
現在我們來分析上面那段代碼:
1、<T>是泛型的標記,當然可以使用別的名字,比如。使用<T>聲明一個泛型的引用,從而可以在class、方法及接口中使用它進行數據定義,參數傳遞。
2、<T>在聲明的時候相當於一個有意義的數據類型,編譯過程中不會發生錯誤;在實例化時,將其用一個具體的數據類型進行替代,從而就可以滿足不用需求。
四、泛型的規則和限制
通過上述的例子,我們簡單理解了泛型的含義。在使用泛型時,請注意其使用規則和限制,如下:
1、泛型的參數類型只能是引用類型,而不能是簡單類型。
比如,<int>是不可使用的。
2、可以聲明多個泛型參數類型,比如<T, P,Q…>,同時還可以嵌套泛型,例如:<List<String>>
3、泛型的參數類型可以使用extends語句,例如<T extends superclass>。
4、泛型的參數類型可以使用super語句,例如< T super childclass>。
5、泛型還可以使用通配符,例如<? extends ArrayList>
五、擴展
1、extends語句
使用extends語句將限制泛型參數的適用範圍。例如:
<T extends collection> ,則表示該泛型參數的使用範圍是所有實現了collection接口的calss。如果傳入一個<String>則程序編譯出錯。
2、super語句
super語句的作用與extends一樣,都是限制泛型參數的適用範圍。區別在於,super是限制泛型參數只能是指定該class的上層父類。
例如<T super List>,表示該泛型參數只能是List和List的上層父類。
3、通配符
使用通配符的目的是爲了解決泛型參數被限制死了不能動態根據實例來確定的缺點。
舉個例子:public class SampleClass < T extends S> {…}
假如A,B,C,…Z這26個class都實現了S接口。我們使用時需要使用到這26個class類型的泛型參數。那實例化的時候怎麼辦呢?依次寫下
SampleClass<A> a = new SampleClass();
SampleClass<B> a = new SampleClass();
…
SampleClass<Z> a = new SampleClass();
這顯然很冗餘,還不如使用Object而不使用泛型,呵呵,是吧?
彆着急,咱們使用通配符,就OK了。
SampleClass<? Extends S> sc = new SampleClass();
只需要聲明一個sc變量,很方便把!
通配符進階
泛型最複雜的部分是對通配符的理解。我們將討論三種類型的通配符以及它們的用途。
首先讓我們瞭解一下數組是如何工作的。可以從一個Integer[]爲一個Number[]賦值。如果嘗試把一個Float寫到Number[]中,那麼可以編譯,但在運行時會失敗,出現一個ArrayStoreException:
Integer[] ia = new Integer[5];
Number[] na = ia;
na[0] = 0.5; // compiles, but fails at runtime
如果試圖把該例直接轉換成泛型,那麼會在編譯時失敗,因爲賦值是不被允許的:
List<Integer> iList = new ArrayList<Integer>();
List<Number> nList = iList; // not allowed
nList.add(0.5);
如果使用泛型,只要代碼在編譯時沒有出現警告,就不會遇到運行時ClassCastException。
上限通配符
我們想要的是一個確切元素類型未知的列表,這一點與數組是不同的。
List<Number>是一個列表,其元素類型是具體類型Number。
List<? extends Number>是一個確切元素類型未知的列表。它是Number或其子類型。
上限
如果我們更新初始的例子,並賦值給List<? extends Number>,那麼現在賦值就會成功了:
List<Integer> iList = new ArrayList<Integer>();
List<? extends Number> nList = iList;
Number n = nList.get(0);
nList.add(0.5); // Not allowed
我們可以從列表中得到Number,因爲無論列表的確切元素類型是什麼(Float、Integer或Number),我們都可以把它賦值給Number。
我們仍然不能把浮點類型插入列表中。這會在編譯時失敗,因爲我們不能證明這是安全的。如果我們想要向列表中添加浮點類型,它將破壞iList的初始類型安全——它只存儲Integer。
通配符給了我們比數組更多的表達能力。
爲什麼使用通配符
在下面這個例子中,通配符用於向API的用戶隱藏類型信息。在內部,Set被存儲爲CustomerImpl。而API的用戶只知道他們正在獲取一個Set,從中可以讀取Customer。
此處通配符是必需的,因爲無法從Set<CustomerImpl>向Set<Customer>賦值:
public class CustomerFactory {
private Set<CustomerImpl> _customers;
public Set<? extends Customer> getCustomers() {
return _customers;
}
}
無界通配符
最後,List<?>列表的內容可以是任何類型,而且它與List<? extends Object>幾乎相同。可以隨時讀取Object,但是不能向列表中寫入內容。
公共API中的通配符
總之,正如前面所說,通配符在向調用程序隱藏實現細節方面是非常重要的,但即使下限通配符看起來是提供只讀訪問,由於remove(int position)之類的非泛型方法,它們也並非如此。如果您想要一個真正不變的集合,可以使用java.util.Collection上的方法,比如unmodifiableList()。
編寫API的時候要記得通配符。通常,在傳遞泛型類型時,應該嘗試使用通配符。它使更多的調用程序可以訪問API。
通過接收List<? extends Number>而不是List<Number>,下面的方法可以由許多不同類型的列表調用:
void removeNegatives(List<? extends Number> list);
構造泛型類型
現在我們將討論構造自己的泛型類型。我們將展示一些例子,其中通過使用泛型可以提高類型安全性,我們還將討論一些實現泛型類型時的常見問題。
集合風格(Collection-like)的函數
第一個泛型類的例子是一個集合風格的例子。Pair有兩個類型參數,而且字段是類型的實例:
public final class Pair<A,B> {
public final A first;
public final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
}
這使從方法返回兩個項而無需爲每個兩種類型的組合編寫專用的類成爲可能。另一種方法是返回Object[],而這樣是類型不安全或者不整潔的。
在下面的用法中,我們從方法返回一個File和一個Boolean。方法的客戶端可以直接使用字段而無需類型強制轉換:
public Pair<File,Boolean> getFileAndWriteStatus(String path){
// create file and status
return new Pair<File,Boolean>(file, status);
}
Pair<File,Boolean> result = getFileAndWriteStatus("...");
File f = result.first;
boolean writeable = result.second;
集合之外
在下面這個例子中,泛型被用於附加的編譯時安全性。通過把DBFactory類參數化爲所創建的Peer類型,您實際上是在強制Factory子類返回一個Peer的特定子類型:
public abstract class DBFactory<T extends DBPeer> {
protected abstract T createEmptyPeer();
public List<T> get(String constraint) {
List<T> peers = new ArrayList<T>();
// database magic
return peers;
}
}
通過實現DBFactory<Customer>,CustomerFactory必須從createEmptyPeer()返回一個Customer:
public class CustomerFactory extends DBFactory<Customer>{
public Customer createEmptyPeer() {
return new Customer();
}
}
泛型方法
不管想要對參數之間還是參數與返回類型之間的泛型類型施加約束,都可以使用泛型方法:
例如,如果編寫的反轉函數是在位置上反轉,那麼可能不需要泛型方法。然而,如果希望反轉返回一個新的List,那麼可能會希望新List的元素類型與傳入的List的類型相同。在這種情況下,就需要一個泛型方法:
<T> List<T> reverse(List<T> list)
具體化
當實現一個泛型類時,您可能想要構造一個數組T[]。因爲泛型是通過擦除(erasure)實現的,所以這是不允許的。
您可以嘗試把Object[]強制轉換爲T[]。但這是不安全的。
具體化解決方案
按照泛型教程的慣例,解決方案使用的是“類型令牌”,通過向構造函數添加一個Class<T>參數,可以強制客戶端爲類的類型參數提供正確的類對象:
public class ArrayExample<T> {
private Class<T> clazz;
public ArrayExample(Class<T> clazz) {
this.clazz = clazz;
}
public T[] getArray(int size) {
return (T[])Array.newInstance(clazz, size);
}
}
爲了構造ArrayExample<String>,客戶端必須把String.class傳遞給構造函數,因爲String.class的類型是Class<String>。
擁有類對象使構造一個具有正確元素類型的數組成爲可能。