小白學Java:老師!泛型我懂了!
泛型概述
使用泛型機制編寫的程序代碼要比哪些雜亂地使用Object變量,然后再進行強制類型轉換地代碼具有更好的安全性和可讀性。
以上摘自《Java核心技術卷一》
在談泛型的定義之前,我先舉一個簡單又真實的例子:如果我想定義一個容器,在容器中放同一類的事物,理所當然嘛。但是在沒有泛型之前,容器中默認存儲的都是Object類型,如果在容器中增加不同類型的元素,都將會被接收,在概念上就不太符合了。關鍵是放進去不同元素之后,會造成一個很嚴重的情況:在取出元素並對里面的元素進行對應操作的時候,就需要復雜的轉型操作,搞不好還會出錯,就像下面這樣:
//原生類型
ArrayList cats = new ArrayList();
cats.add(new Dog());
cats.add(new Cat());
for (int i = 0; i < cats.size(); i++) {
//下面語句類型強轉會發生ClassCastException異常
((Cat) cats.get(i)).catchMouse();
}
而泛型又是怎么做的呢?通過尖括號<>
里的類型參數來指定元素的具體類型。
ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new SkyBarking());
dogs.add(new Snoopy());
dogs.add(new Cat());//編譯不通過
//向上轉型,另外兩個是Dog的子類對象
for(Dog d:dogs){
System.out.println(d);
}
至此,泛型優點顯而易見:
- 可讀性:很明顯嘛,一看就知道是存着一組Dog對象。
- 安全性:如果類型不符,編譯不會通過,因此不再需要進行強制轉換。
妙啊,從中我們可以體會泛型的理念:泛型只存在編譯器,寧可讓錯誤發生在編譯期,也不願意讓程序在運行時出現類型轉換異常。 因為bug發生在編譯期更容易去找到並修復。 除此之外:
- 可將子類類型傳入父類對象的容器之中,向上轉型。
- 不必糾結對象的類型,可用增強for循環實現遍歷。
定義泛型
再次強調,所謂泛型,即參數化類型,就是只有在使用類的時候,才把類型確定下來,相當的靈活。
泛型類的定義
class Element<T>{
private T value;
Element(T value){
this.value = value;
}
public T getvalue() {
return this.value;
}
}
- 引入類型變量T(按照規范,也可以有多個,用逗號隔開),並用
<>
擴起,放在類名后面。 - 其實就是可以把T假想成平時熟悉的類型,這里只不過用個符號代替罷了。
Element<String> element = new Element<>("天喬巴夏");
System.out.println(element.getvalue());
- 使用泛型時,用具體類型(只能是引用類型)替換類型變量T即可。泛型其實可以堪稱普通類的工廠。
- 泛型接口的定義與類定義類似,就暫且不做贅述。
泛型方法的定義
class ParaMethod {
public static <T> T getMiddle(T[] a) {
return a[a.length/2];
}
}
- 注意該方法並不是在泛型類中所定義,而是在普通類中定義的泛型方法。
- 類型變量T放在修飾符的后面,返回類型的前面,只是正好我們這邊返回類型也是T。
int m = ParaMethod.getMiddle(new Integer[]{1,2,3,4,5});
//返回Integer類型,自動拆箱
System.out.println(m);//3
類型變量的限定
我們上面講到,泛型擁有足夠的靈活性,意味着我傳啥類型,運行的時候就是啥類型。但是,實際生活中,我要是想對整數類型進行操作,不想讓其他類型混入,怎么辦呢?對了,加上類型限定。
public static <T extends Number> T getNum(T num) {
return num;
}
- 定義格式:
修飾符 <T extends 類型上限> 返回類型 方法名 參數列表
,如上表示對類型變量的上限進行限定,只有Number及其子類可以傳入。 - 規定類型的上限的數量最多只能有一個。
- 既然類的定義是這樣子,那大膽猜測一下,定義接口上線是不是就應該用
implements
關鍵字呢?答案是:否!接口依舊也是extends
。
public static <T extends Comparable & Serializable> T max(T[] a) {
if (a == null || a.length == 0) return null;
T maximum = a[0];
for (int i = 1; i < a.length; i++) {
if (maximum.compareTo(a[i]) < 0) maximum = a[i];
}
return maximum;
}
- 需要注意的是:如果允許多個接口作為上限,接口可以用&隔開。
- 如果規定上限時,接口和類都存在,類需要放在前面,
<T extends 類&接口>
。 - 沒有規定上限的泛型類型可以視為:
<T extends Object>
。
原生類型與向后兼容
使用泛型類而不指定具體類型,這樣的泛型類型就叫做原生類型(raw type),用於和早期的Java版本向后兼容,畢竟泛型JDK1.5之后才出呢。其實我們在本篇開頭舉的例子就包含着原生類型,ArrayList cats = new ArrayList();
。
ArrayList cats = new ArrayList();//raw type
它大致可以被看成指定泛型類型為Object的類型。
ArrayList<Object> cats = new ArrayList<Object>();
注意:原生類型是不安全的!因為可能會引發類型轉換異常,上面已經提到。所以我們在使用過程中,盡量不要使用原生類型。
通配泛型
我們通過下面幾個例子,來詳細總結通配類型出現的意義,以及具體的用法。
非受限通配
如果我想定義一個方法,讓它接收一個集合,不關注集合中元素的類型,並把集合中的元素打印出來,應該怎么辦呢?
上面談到泛型,你可能會這樣寫,讓方法接收一個Object的集合,這樣子你傳進來啥我都接,完成之后美滋滋,一調試就不對了:
public static void print(ArrayList<Object> arrayList){
//錯誤!:arrayList.add(5);
for(int i = 0;i< arrayList.size();i++){
System.out.println(arrayList.get(i));
}
}
ArrayList<Integer> arr = new ArrayList<>();
print(arr);
究其原因:Integer是Object的子類的確沒錯,但是ArrayList