深入理解Java泛型機制

簡介

泛型的意思就是參數化類型,通過使用參數化類型創建的接口、類、方法,可以指定所操作的數據類型。比如:可以使用參數化類型創建操作不同類型的類。操作參數化類型的接口、類、方法成為泛型,比如泛型類、泛型方法。
泛型還提供缺失的類型安全性,我們知道Object是所有類的超類,在泛型前通過使用Object操作各種類型的對象,然后在進行強制類型轉換。而通過使用泛型,這些類型轉換都是自動或隱式進行的了。因此提高了代碼重用能力,而且可以安全、容易的重用代碼。

泛型類

<pre>
public class Generic<T> {
T ob;
Generic(T o){
this.ob = o;
}
T getOb(){
return ob;
}
void showType(){
System.out.println("T type:" + ob.getClass().getName());
}
}
</pre>
<pre>
class Generic<T>
</pre>
T是類型參數名稱,使用<>括上,這個名稱是實際類型的占位符。當創建一個Generic對象的時候,會傳遞一個實際類型,因為Generic使用了類型參數,所以該類是泛型類。類中只要需要使用類型參數的地方就使用T,當傳遞實際類型后,會自動改變成實際類型。
比如:
T的類型就是Integer。
<pre>
Generic<Integer> gen1 = new Generic<Integer>(100);
</pre>
T的類型就是String。
<pre>
Generic<String> gen2 = new Generic<String>(“test”);
</pre>

使用泛型類

當調用泛型構造方法時候,仍然需要指定參數類型,因為為構造函數賦值的是Generic<String>。
需要注意上述這個過程,就像Java編譯器創建了不同版本的Generic類,但實際編譯器并沒有那樣做,而是將所有泛型類型移除,進行類型轉換,從而看似是創建了一個個Generic類版本。移除泛型的過程稱為擦除。

泛型只能使用引用類型

當聲明泛型實例的時候,傳遞過來的類型參數必須引用類型。不能是基本類型,比如int、char等。其實可以通過類型封裝器封裝基本類型,所以這個限制并不嚴格。
<pre>
Generic<int> gen3 = new Generic<int>();
</pre>

基于不同類型的泛型類是不同的,比如Generic<Integer> gen1和Generic<String> gen2雖然都是Generic<T>類型,但是它們是不同的類型引用,所以gen1 != gen2。這個就是泛型添加類型安全以及防止錯誤的一部分。

泛型類型安全的原理

上面我們說過,其實泛型的實現完全可以通過使用Object類型替換,將Genneric中所有T轉換成Object類型,然后在使用時候通過強制類型轉換獲取值。但是這有許多風險的,比如手動輸入強制類型轉換、進行類型檢查。而實用泛型它會將這些操作將是隱式完成的,泛型能夠保證自動確保類型安全。可以將運行時錯誤轉換成編譯時錯誤,比如如果實用Object替代泛型,對于之前Generic<Integer> gen1 和Generic<String> gen2,將gen1 = gen2這樣在泛型中直接編譯錯誤,如果使用Object替代,則不會產生編譯錯誤,因為它們本身都是Generic類型,但是在執行相關代碼時候會出錯,比如getOb()將String類型直接賦值給int類型。

多個類型參數的泛型類

當需要聲明多個參數類型時,只需要使用逗號分隔參數列表即可。
<pre>
public class Generic<T,V> {
T ob1;
V ob2;
Generic(T ob1,V ob2){
this.ob1 = ob1;
this.ob2 = ob2;
}
T getOb1(){
return ob1;
}
V getOb2(){
return ob2;
}
void showType(){
System.out.println("T type:" + ob1.getClass().getName());
System.out.println("V type:" + ob2.getClass().getName());
}
}
</pre>
這樣在創建Generic實例時候,需要分別給出參數類型。
<pre>
Generic<String,Integer> generic = new Generic<String,Integer>(“test”,123);
</pre>
泛型類定語法:
<pre>
class class-name<type-param-list>{
//….
}
</pre>
泛型類引用語法:
<pre>
class-name<type-param-list> var-name = new class-name<type-param-list>(con-arg-list);
</pre>

有界類型(bounded type)

前面討論的泛型,可以被任意類型替換。對于絕大多數情況是沒問題的,但是一些特殊場景需要對傳遞的類型進行限制,比如一個泛型類只能是數字,不希望使用其它類型。我們知道無論Integer還是Double都是Number的子類,所以可以限制只有Number及其子類可以使用,定義的泛型類的時候,在泛型類中可以使用Number中定義的方法(否則無法使用,比如使用Number類中的doubleValue(),如果直接使用會無法通過編譯,因為T泛型,并不知道你這個參數類型是什么)。

<pre>
public class Generic<T extends Number> {
T[] array;
Generic(T[] array){
this.array = array;
}
double average(){
double sum = 0;
for(int i=0;i<array.length;i++){
sum += array[i].doubleValue();
}
return sum / array.length;
}
}

Generic<T extends Number>
</pre>

這樣T只能被Number及其子類代替,這時候java編譯器也知道T類型的對象都可以調用dobuleValue()方法,因為這個方法是Number中定義的。
除了可以使用類作為邊界,也可以使用接口作為邊界,使用方式與上面相同。同時也可以同時使用一個類和一個接口或多個接口邊界,對于這種情況,需要先指定類類型。如果指定接口類型,那么實現了這個接口的類型參數是合法的。
<pre>
class class-name<T extends MyClass & MyInterface>
</pre>

使用通配符參數

我們繼續擴展上面這個類,當需要一個sameAvg()方法用來比較兩個對象的average()接口是否相同,這個sameAvg()接口怎么寫?
第一種方式:
<pre>
boolean sameAvg(Generic<T> ob){
if(average() == ob.average())
return true;
return false;
}
</pre>
這種方式有一個弊端,就是Generic<Integer>只能和Generic<Integer>比較(上面說了),而我們比較相同平均數并care類型。這時我們可以使用通配符“?”來解決。
第二種方式:
<pre>
boolean sameAvg(Generic<?> ob){
//...
}
</pre>

使用通配符需要理解一點,它本身不會影響創建什么類型的Generic對象,通配符只是簡單匹配所有有效的(有界類型下的)Generic對象。

有界通配符

使用有界通配符,可以為參數類型指定上界和下界,從而能夠限制方法能夠操作的對象類型。最常用的是指定有界通配符上界,使用extends子句創建。

<pre>
<? extends superclass>
</pre>
這樣直有superclass類及其子類可以使用。也可以指定下界:
<pre>
<? super subclass>
</pre>

這樣subclass的超類是可接受的參數類型。
有界通配符的應用場景一般是操作類層次的泛型(C 繼承 B,B繼承A),控制層次類型。

創建泛型方法

之前討論泛型類中的泛型方法都是使用創建實例傳遞過來的類型,其實方法可以本身使用一個或多個類型參數的泛型方法。并且,可以在非泛型類中創建泛型方法。
<pre>
class GenericDemo {
<T extends Comparator<T>, V extends T> boolean isIn(T x, V[] y) {
for (int i = 0; i < y.length; i++) {
if (x.equals(y[i]))
return true;
}
return false;
}
}
</pre>
<pre>
<T extends Comparator<T>, V extends T> boolean isIn(T x, V[] y)
</pre>
泛型參數在返回類型之前,T擴展了類型Comparator<T>,所以只有實現了Comparator<T>接口的類才可以使用。同時V設置了T為上界,這樣V必須是T或者其子類。通過強制參數,達到相互兼容。
調用isIn()時候一般可以直接使用,不需要指定類型參數,類型推斷就可以自動完成。當然你也可以指定類型:
<pre>
<Integer,Integer>isIn(3,nums);
</pre>
泛型方法語法:
<pre>
<type-param-list> ret-type meth-name(param-list){
//..
}
</pre>
也可以為構造方法泛型化,即便類不是泛型類,但是構造方法是。所以在構造該實例時候需要根據泛型類型給出。
<pre>
<T extends Number> Generic(T a){
//..
}
</pre>

泛型接口

泛型接口與定義泛型類是類似的
<pre>
interface MyInterface<T extends Comparable<T>>{
//...
}
</pre>
當類實現接口時候,因為接口指定界限,所以實現類也需要指定相同的界限。并且接口一旦建立這個界限,那么在實現他的時候就不需要在指定了。
<pre>
class MyClass<T extends Compareable<T>> implements MyInterface<T>{
//...
}
</pre>
如果類實現了具體類型的泛型接口,實現類可以不指出泛型類型。
<pre>
class MyClass implements MyInterface<Integer>{
//...
}
</pre>
使用泛型接口,可以針對不同類型數據進行實現;使用泛型接口也為實現類設置了類型限制條件。
定義泛型接口語法:
<pre>
interface interface-name<type-param-list>{
//...
}
</pre>
實現泛型接口
<pre>
class class-name<type-params-list> implements interface-name<type-arg-list>{
//...
}
</pre>

遺留代碼中的原始類型

泛型是在JDK 5之后提供的,在JDK 5之前是不支持的泛型的。所以這些遺留代碼即需要保留功能,又要和泛型兼容。可以使用混合編碼,還比如上面的例子。Generic<T> 類是一個泛型類,我們可以使用原始類型(不指定泛型類型),來創建Generic類。
<pre>
Generic gen1 = new Generic(new Double(9.13));
double gen2 = (Double)gen1.getOb();
</pre>
java是支持這種原始類型,然后通過強制類型轉換使用的。但是正如我們上面說的,這就繞過了泛型的類型檢查,它是類型不安全的,有可能導致運行時異常(RunTime Exception)。

泛型類層次

泛型類也可以是層次的一部分,就像非泛型類那樣。泛型類可以作為超類或子類。泛型和非泛型的區別在于,泛型類層次中的所有子類會將類型向上傳遞給超類。
<pre>
class Gen<T>{
T ob;
Gen(T ob){
this.ob = ob;
}
T genOb(){
return ob;
}
}
class Gen2<T> extends Gen<T>{
Gen2(T o){
super(o);//向上傳遞
}
}
</pre>
<pre>
Gen2<Integer> gen = new Gen2<Integer>();
</pre>
創建Gen2傳入Integer類型,Integer類型也會傳入超類Gen中。子類可以根據自己的需求,任意添加參數類型。
<pre>
class Gen2<T,V> extends Gen<T>{
Gen2(T a,V b){
super(a);//一定要有
}
}
</pre>
超類也可以不是泛型,子類在繼承的時候,就不需要有特殊的條件了。
<pre>
class Gen{
Gen(int a){
}
}
class Gen2<T,V> extends Gen{
Gen2(T a,int b){
super(b);
}
Gen2(T a,V b,int c){
super(c);
}
}
</pre>
需要注意的:

  • 泛型類型強制類型轉換,需要兩個泛型實例的類型相互兼容并且它們的類型參數也相同。
  • 可以向重寫其它方法那樣重寫泛型的方法。
  • 從JDK 7起泛型可以使用類型推斷在創建實例時候省略類型,因為在參數聲明的時候已經指定過一次了,所以可以根據聲明的變量進行類型推斷。
    List<String,Integer> list = new ArrayList<>();

擦除

泛型為了兼容以前的代碼(JDK 5之前的),使用了擦除實現泛型。具體就是,當編譯java代碼的時候,所有泛型信息被移除(擦除)。會使用它們的界定類型替換,如果沒有界定類型,會使用Object,然后進行適當的類型轉換。

模糊性錯誤

泛型引入后,也增加了一種新類型錯誤-模糊性錯誤的可能,需要進行防范。當擦除導致兩個看起來不同的泛型聲明,在擦除之后可能變成相同類型,從而導致沖突。
<pre>
class Gen<T,V>{
T ob1;
V ob2;
void setOb(T ob){
this.ob1 = ob;
}
void setOb(V ob){
this.ob2 = ob;
}
}
</pre>
這種是無法編譯的,因為當擦除后可能會導致類型相同,這樣的方法重載是不對的。
<pre>
Gen<String,String> gen = new Gen<String,String>();
</pre>
這樣T和V都是String類型,明顯代碼是不對的。
可以通過指定一個類型邊界,比如:
<pre>
class Test1{
public static void main(String[] args){
//沒問題
Gen<String,Integer> gen = new Gen<String, Integer>();
gen.setOb(1);
//這樣在調用setOb的時候也會編譯失敗,因為都為Integer類型,方法重載錯誤
Gen<Integer,Integer> gen1 = new Gen<Integer, Integer>();
gen1.setOb(1);
}
}
</pre>
所以在解決這種模糊錯誤時候,最好使用獨立的方法名,而不是去重載。

使用泛型的限制

  • 不能實例化類型參數,因為編譯器不知道創建哪種類型,T只是類型占位符。
    <pre>
    class Gen<T>{
    T ob;
    Gen(){
    ob = new T();
    }
    }
    </pre>
  • 靜態成員不能使用類中聲明的類型參數。
    <pre>
    class Gen<T>{
    //錯誤的,不能聲明靜態成員
    static T ob;
    //錯誤的,靜態方法不能使用參數類型T
    static T getGen(){
    return ob;
    }
    //正確的,靜態方法不是參數類型
    static void printXXX(){
    System.out.println();
    }
    }
    </pre>
  • 不能實例化類型參數數組
    <pre>
    //沒問題
    T[] vals;
    //不能實例化類型參數數組
    vals = new T[10];
    </pre>
  • 不能創建特性類型的泛型應用數組
    <pre>
    //這是不允許的
    Gen<Integer> gen = new Gen<Integer>[10];
    但是可以使用通配符,并且比使用原始類型好,因為進行了類型檢查。
    Gen<?> gen = new Gen<?>[10];
    </pre>
  • 泛型類不能擴展Throwable,這就意味著不嗯滾創建泛型異常類。

關注我

歡迎關注我的公眾號,會定期推送優質技術文章,讓我們一起進步、一起成長!
公眾號搜索:data_tc
或直接掃碼:??


歡迎關注我
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容

  • 在之前的文章中分析過了多態,可以知道多態本身是一種泛化機制,它通過基類或者接口來設計,使程序擁有一定的靈活性,但是...
    _小二_閱讀 694評論 0 0
  • object 變量可指向任何類的實例,這讓你能夠創建可對任何數據類型進程處理的類。然而,這種方法存在幾個嚴重的問題...
    CarlDonitz閱讀 927評論 0 5
  • Java泛型總結# 泛型是什么## 從本質上講,泛型就是參數化類型。泛型十分重要,使用該特性可以創建類、接口以及方...
    kylinxiang閱讀 929評論 0 1
  • 今年春節假期參與了鄭州十點讀書會組織的共讀活動,我讀的是蔣勛老師的《品味四講》,一共用了十天的時間才斷斷續續的看完...
    正齊讀道閱讀 646評論 3 0
  • 下午戶外活動是夾球跳,龍龍經過前幾周玩耍的經驗龍龍已經掌握夾球的秘訣啦,剛拿到球就能輕松的把球給夾起來,并夾著球跳...
    a81c671c0ae2閱讀 276評論 0 0