Java基礎(六)深入解讀泛型(1)
一名合格的Java程式設計師,當然要經常翻翻JDK的原始碼。經常看JDK的API或者原始碼,我們才能更加了解JDK,才能更加熟悉底層。
一、引出泛型
然而,在看原始碼的過程中,我們經常會看到類似於如下這樣的程式碼:
private void putAllForCreate(Map<? extends K, ? extends V> m) { for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) putForCreate(e.getKey(), e.getValue()); }
上面程式碼出自HashMap中的一段。程式碼中的泛型,雖然不影響我們看懂這段程式碼,但是屬於半個處女座的我,還是有著強烈的強迫症,就有那麼一股衝動,慫恿著我把它搞清楚。
我們可以觀察一下上面的程式碼,“? extends K”,很明顯,這應該是某種繼承關係的體現,但是什麼時候會用到這種泛型裡的繼承關係呢?除了這種泛型關係,你應該還見過一些其他的表現形式,那麼好,如果你想搞清楚這些,就好好看這篇文章吧,我相信:搞清楚這些,一定會對你做研發時,封裝程式碼,有很大的幫助。
二、泛型的定義
泛型,大家在學習,工作中,一般都會多多少少的接觸過一些。所以,關於定義,我們長話短說,幫大家複習一下即可。
泛型是在JDK1.5增加的,主要用來標記Java集合中元素的資料型別。怎麼講呢?
在JDK1.5之前,一旦把一個物件丟進Java集合中,集合就會忘記物件的型別,把所有的物件當成Object型別處理。當程式衝集合中取出物件時,就需要進行強制型別轉換,這種做法,不僅程式碼看起來很臃腫,而且容易引發型別轉換異常ClassCastException。
而通常情況下使用集合,一個集合,只存放同一型別的東西。即一個定義了的String型別的List,不能盛放Integer型別的資料。
而泛型,可以標記或規定集合中元素的型別,並且在編譯時,檢查其型別。
三、泛型的應用
1、定義泛型介面、類
所謂泛型:就是允許在定義類、介面時,指定型別形參,這個型別形參將在宣告變數、建立物件時確定(即傳入實際的型別引數,也可以成為型別實參)。
搞個例子看看,List、ListIterator、Map介面,都是在定義時指定型別,而型別的確定是在建立變數的時候,如List<String> list= new ArrayList<String>(),建立list變數時,確定型別實參為String型別:
//定義介面時指定了一個型別形參,該形參名為E
public interface ListA<E> extends Collection<E> {
//在該接口裡,E可作為型別使用
boolean add(E e); //引數型別
ListIterator<E> listIterator();
}
//定義介面時,指定你了一個型別形參,該形參名為E
public interface ListIterator<E> extends Iterator<E>{
//在該接口裡E完全可以作為型別使用
E next();
E previous();
void set(E e);
}
//定義該介面時,指定了兩個型別形參,其形參名為K、V
public interface Map<K,V>{
//在該接口裡K,V完全可以作為型別使用
Set<K> keySet();
V put(K key, V value);
void putAll(Map<? extends K, ? extends V> m);
}
這就是泛型的實質:允許在定義介面、類時,指定型別形參,型別形參在整個介面、類體內可當成型別使用,幾乎所有可使用其他普通型別的地方,都可以使用這個型別形參。
上面方法宣告返回值型別:ListIterator<E>、Set<K>,這表明Set<K>形式 是一種特殊的資料型別,是一種與Set不同的資料型別——我們可以認為Set<K>是Set型別的子類。
例如,我們使用List型別時,為E形參傳入String型別實參,則產生了一個新的型別:List<String>型別,我們可以把List<String>想象成E被全部替換成String的特殊List子介面。
我們可以把List<String>想象成E被全部替換成String的特殊List子介面:
//List<String>等同於如下介面
public interface ListString extends List {
//原來的E形參全部變成String型別實參
boolean add(String e); //引數型別
ListIterator<String> listIterator();
}
通過上面這種方式,解決了一個問題:雖然程式只定義了一個List<E>介面,但實際使用時,可以產生無數多個List介面,只要為E傳入不同的型別實參,系統就會多出一個新的List子介面。如List<String>,List<Integer>,List<Long>,List<Boolean>等等。
當然,List<String>絕不會被替換成ListString,系統沒有進行原始碼複製,二進位制程式碼中沒有,磁碟沒有,記憶體中也沒有。
PS:包含泛型宣告的型別可以在定義變數、建立物件時,傳入一個型別實參,從而可以動態生成無數多個邏輯上的子類,但這種子類在物理上並不存在。
搞個例項:
//定義Apple類時使用了泛型宣告
public class Apple<E> {
//使用E型別形參定義屬性
private E info;
public Apple() {}
//下面方法中使用E型別形參來定義方法
public Apple(E info) {
this.info = info;
}
public E getInfo() {
return info;
}
public void setInfo(E info) {
this.info = info;
}
public static void main(String[] args) {
//因為傳給T形參的是String實際型別,所以構造器的引數只能是String
Apple<String> a1 = new Apple<String>("蘋果");
System.out.println(a1.getInfo());
//因為傳給T形參的是Double實際型別,所以構造器的引數只能是Double或double
Apple<Double> a2 = new Apple<Double>(5.67);
System.out.println(a2.getInfo());
}
}
上面程式定義了一個帶泛型宣告的Apple<T>類,而在Main函式中,實際使用Apple<T>類時會為T形參傳入實際型別,這樣就可以生成如Apple<String>、Apple<Double>……形式的多個邏輯子類(物理上並不存在),這時建立對應的邏輯形參。
當建立帶泛型宣告的自定義類,為該類定義構造器時,構造器還是原來的類名,不要增加泛型宣告。例如為Apple<T>類定義構造器,其構造器名依然是Apple,而不是Apple<T>,但呼叫該構造器時,卻可以使用Apple<T>的形式,當然應該為T形參傳入實際的型別引數。
2、從泛型類派生子類
當建立了帶泛型宣告的介面、父類之後,可以為該介面建立實現類,或從父類派生子類,但是:當使用這些介面、父類時不能再包含型別形參。
例如下面程式碼是錯誤的:
//定義類A繼承Apple類, Apple類後面不能跟型別形參
public class A extends Apple<E> {}
注意:方法中的形參,只有當定義方法時,才可以使用資料形參,當呼叫方法時,必須為這些資料形參傳入實際的資料;與此類似的是:類、介面中的型別形參,只有在定義類、介面時,才可以使用型別形參,當使用類、介面時,應為型別形參傳入實際的型別。
所以,總結起來就是:方法、類、介面 中的型別形參,只有在定義時,才可以使用型別形參;在使用時,應為型別形參傳入實際的型別。
所以,上面的程式碼可以改為如下(當然刪除後面的String也是可以的):
//使用Apple類時,為T形參傳入String型別
public class A extends Apple<String> {}
或
public class A extends Apple {}
如果從Apple<String>類派生子類,則在Apple類中所有使用E型別形參的地方都將被替換成String型別,即它的子類將會繼承到String getInfo() 和 void setInfo(String info)兩個方法,如果子類需要重寫父類的方法,必須注意這一點。
搞個例子,解釋一下:
public class subApple extends Apple<String> {
//正確重寫了父類的方法,返回值與父類Apple<String>的返回值完全相同
public String getInfo(){
return "子類:"+super.getInfo();
}
// 下面方法是錯誤的,重寫父類方法時,返回值型別不一致
// public Object getInfo(){
// return "子類";
// }
}
如果使用Apple類時沒有傳入實際的引數型別,系統會將Apple<E>類裡的E形參當成Object型別處理。
上面例子是帶泛型宣告的父類派生子類,建立帶泛型宣告的介面的實現類於此幾乎完全一樣。
3、不存在泛型類
ArrayList<String>類,是一種特殊的ArrayList類,這個ArrayList<String>物件只能新增String物件作為集合元素。
但實際上,系統並沒有為ArrayList<String>生成新的class檔案,而且也不會把ArrayList<String>當成新類來處理。
下面看個例子:
import java.util.ArrayList;
import java.util.List;
public class CommonTest {
public static void main(String[] args) {
List<String> lst1 = new ArrayList<String>();
List<Integer> lst2 = new ArrayList<Integer>();
Boolean flag = false;
flag = lst1.getClass() ==lst2.getClass();
//true
System.out.println(flag);
}
}
上面程式的輸出結果是true。因為不管泛型型別的實際型別引數是什麼,它們在執行時總有同樣的類(class)。
實際上,泛型對其所有可能的型別引數,都具有同樣的行為,從而可以把相同的類當成許多不同的類來處理。
與此完全一致的是:類的靜態變數和方法也在所有例項間共享,所以在靜態方法、靜態初始化 或者 變數的宣告 和初始化 中,不允許使用型別形參。
下面程式演示了這種錯誤:
public class Test<E> {
E age;
//下面程式碼錯誤,不能再靜態屬性宣告中使用型別形參
// static E info;
public void foo(E msg){}
//下面程式碼錯誤,不能在靜態方法宣告中使用型別形參
// public static void bar(E msg){}
}
由於系統不併不會正真生成泛型類,所以instanceof運算後,不能使用泛型類,如下程式碼是錯誤的:
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Test2 {
public static void main(String[] args) {
Collection<String> cs = new ArrayList<String>();
//下面程式碼編譯時引發錯誤:instanceof運算子後不能使用泛型類
// if (cs instanceof List<String>) {}
}
}
本文初步講解了泛型基礎內容,下一篇繼續深入探討。