Java的泛型解析

一、為什么要使用泛型

1.類型參數的好處

  • 類型安全:泛型的主要目標是提高 Java 程序的類型安全。通過知道使用泛型定義的變量的類型限制,編譯器可以在一個高得多的程度上驗證類型假設。沒有泛型,這些假設就只存在于程序員的頭腦中(或者如果幸運的話,還存在于代碼注釋中)。

  • 消除強制類型轉換:泛型的一個附帶好處是,消除源代碼中的許多強制類型轉換。這使得代碼更加可讀,并且減少了出錯機會。

Java語言引入泛型的好處是安全簡單。泛型的好處是在編譯的時候檢查類型安全,并且所有的強制轉換都是自動和隱式的,提高代碼的重用率。


二、定義簡單的泛型類

泛型類的定義比較簡單,如下便可以定義一個泛型類,在實例化泛型類的時候必須指明泛型的具體類型。

public class Pair<T>{
    private T first;
    private T second;
    
    public Pair(){
        first = null;second = null;
    }
    
    public T getFirst(){
        return first;
    }
    
    public T getSecond(){
        return second;
    }
    
    public void setFirst(T newValue){
        first = newValue;
    }
    public void setSecond(T newValue){
        second = newValue;
    }
}

泛型在使用中還有一些規則和限制:

  • 泛型的類型參數只能是類類型(包括自定義類),不能是簡單類型。
  • 同一種泛型可以對應多個版本(因為參數類型是不確定的),不同版本的泛型類實例是不兼容的。
  • 泛型的類型參數可以有多個。
  • 泛型的參數類型可以使用extends語句,例如<T extends superclass>。習慣上成為“有界類型”。
  • 泛型的參數類型還可以是通配符類型。例如Class<?> classType = Class.forName(Java.lang.String);

泛型類可以定義多個類型變量,例如

public class Pari<T,U>{
    ...
}

三、泛型方法

Java中的泛型方法相對復雜一點,在調用的時候需要指明泛型類型

定義泛型的語法:

image

調用泛型的語法:

image

定義泛型方法時,必須在返回值前邊加一個<T>,來聲明這是一個泛型方法,持有一個泛型T,然后才可以用泛型T作為方法的返回值。注意:類型變量放在修飾符的后面,返回類型的前面。

既然是泛型方法,就代表著我們不知道具體的類型是什么,也不知道構造方法如何,因此沒有辦法去new一個對象,但可以利用變量c的newInstance方法去創建對象,也就是利用反射創建對象。

泛型方法要求的參數是Class<T>類型,而Class.forName()方法的返回值也是Class<T>,因此可以用Class.forName()作為參數。其中,forName()方法中的參數是何種類型,返回的Class<T>就是何種類型。在本例中,forName()方法中傳入的是User類的完整路徑,因此返回的是Class<User>類型的對象,因此調用泛型方法時,變量c的類型就是Class<User>,因此泛型方法中的泛型T就被指明為User,因此變量obj的類型為User。


四、類型變量的限定

我們都知道在方法前指定了<T>,那么就是說這個泛型類型和類定義時的泛型類型無關,所以可以在普通類中定義泛型方法,泛型可以限定類型變量必須實現某幾種接口或者繼承某個雷,多個限定類型通過&分隔,如:

public static <T extends Comparable> T min(T[] a)...

對泛型進行限制,使其只有集成或實現Comparable的類才能使用該方法

(1) ? extends X:表示類型的上界

特點:

  • 限定 ? 為 X 的子類型,但不知道是哪個子類型
  • 可以安全的訪問數據,訪問X及其子類型
<T extends BoundingType>

T表示綁定類型的子類型,T和綁定類型可以是類或者接口。

一個變量或者通配符可以綁定多個限定,用“&”分開

T extends Comparable & Serializable

若T的限定類型是類,則有且最多只有一個,且放于接口前面


五、泛型代碼和虛擬機

Java虛擬機是不存在泛型類型對象的,所有的對象都屬于普通類,甚至在泛型實現的早起版本中,可以將使用泛型的程序編譯為在1.0虛擬機上能夠運行的class文件,這個向后兼容性后期被拋棄了,所以后來如果用Sun公司的編譯器編譯的泛型代碼,是不能運行在Java5.0之前的虛擬機的,這樣就導致了一些實際生產的問題,如一些遺留代碼如何跟新的系統進行銜接,要弄明白這個問題,需要先了解一下虛擬機是怎么執行泛型代碼的。

1.類型擦除

類型擦除指的是通過類型參數合并,將泛型類型實例關聯到同一份字節碼上。編譯器只為泛型類型生成一份字節碼,并將其實例關聯到這份字節碼上。類型擦除的關鍵在于從泛型類型中清除類型參數的相關信息,并且再必要的時候添加類型檢查和類型轉換的方法。

虛擬機的一種機制:擦除類型參數,并將其替換成限定類型,沒有限定類型用Object代替

public class Period<T extends Comparable<T> & Serializable> {  
      private T begin;  
      private T end;  
  
      public Period(T one, T two) {  
               if (one.compareTo(two) > 0) {begin = two;end = one;  
              } else {begin = one;end = two;}  
     }  
}  

//擦除后
public class Period implements Serializable{  
      private Comparable begin;  
      private Comparable end;  
  
      public Period(Comparable one, Comparable two) {  
               if (one.compareTo(two) > 0) {begin = two; end = one;  
              } else {begin = one; end = two;}  
     }  
}  

Java泛型的處理幾乎都在編譯器中進行,編譯器生成的字節碼是不包涵泛型信息的,泛型類型信息將在編譯處理是被擦除,這個過程即類型擦除。通常情況下,Java是通過以下方式處理泛型:Java編譯器通過Code sharing方式為每個泛型類型創建唯一的字節碼表示,并且將該泛型類型的實例都映射到這個唯一的字節碼表示上。將多種泛型類形實例映射到唯一的字節碼表示是通過類型擦除(type erasue)實現的。

Code sharing:對每個泛型類只生成唯一的一份目標代碼;該泛型類的所有實例都映射到這份目標代碼上,在需要的時候執行類型檢查和類型轉換。

注意:
當存在情況:class Interval<T extends Serializable & Comparable> ,原始類型用Serializable替換T,在有必要的時候向Comparable強制類型轉換,為了提高效率,應該將沒有方法的接口放在列表的后面。

類型擦除帶來的靈異問題:

  • 無法用同一泛型類型的實例區分方法簽名
    public class Erasure{  
  
            public void test(List<String> ls){  
                System.out.println("Sting");  
            }  
            public void test(List<Integer> li){  
                System.out.println("Integer");  
            }  
    }
image
image
  • 不能同時catch同一泛型異常類的多個實例
  • 泛型類的靜態變量是可以共享的
import java.util.*;  
  
public class StaticTest{  
    public static void main(String[] args){  
        GT<Integer> gti = new GT<Integer>();  
        gti.var=1;  
        GT<String> gts = new GT<String>();  
        gts.var=2;  
        System.out.println(gti.var);  
    }  
}  
class GT<T>{  
    public static int var=0;  
    public void nothing(T x){}  
}

//輸出2

2.翻譯泛型表達式

Couple<Employee> couple = ...;  
Employee wife = couple.getWife();  

擦除后,getWife()返回的是Object類型,然后虛擬機會插入強制類型轉換,將Object轉換為Employee,所以虛擬機實際上執行了兩天指令:

  • 1.調用Couple.getWife()方法。
  • 2.將Object轉換成Employee類型。

3.翻譯泛型方法

public static <T extends Comparable<T>> max(T[] arrays) {... }  
擦除后成了:  
public static Comoparable max(Comparable[] arrays) {... }  
public class Period <T extends Comparable<T> & Serializable> {  
      private T begin;  
      private T end;  
  
      public Period(T one, T two) {  
               if (one.compareTo(two) > 0) {begin = two;end = one;  
              } else {begin = one;end = two;}  
     }  
     public void setBegin(T begin) {this. begin = begin;}  
     public void setEnd(T end) {this. end = end;}  
     public T getBegin() {return begin;}  
     public T getEnd() {return end;}  
}  
public class DateInterval extends Period<Date> {  
  
      public DateInterval(Date one, Date two) {  
               super(one, two);  
     }  
      public void setBegin(Date begin) {  
               super.setBegin(begin);  
     }  
}  

DateInterval類型擦除后,Period中的方法變成:

  • public void setBegin(Object begin) {...}

而DateInterval中的方法還是:

  • public void setBegin(Date begin) {...}

所以DateInterval從Period中繼承了 public void setBegin(Object begin) {...}而自身又存在public void setBegin(Date begin) {...}方法,用戶使用時問題發生了:

Period<Date> period  = new DateInterval(...);  
period.setBegin(new Date());  

這里因為period引用指向了DateInterval實例,根據多態性,setBegin應該調用DateInterval對象的setBegin方法,可是這個擦除讓Period中的 public void setBegin(Object begin) {...}被調用,導致了擦除與多態發生了沖突,怎么辦呢?虛擬機此時會在DateInterval類中生成一個橋方法(bridge method),調用過程發生了細微的變化:

public void setBegin(Object begin) {  
     setBegin((Date)begin);  
 }  

有了這個合成的橋方法以后,code07中對setBegin的調用步驟如下:

1.調用DateInterval.setBegin(Object)方法。
2.DateInterval.setBegin(Object)方法調用DateInterval.setBegin(Date)方法。

發現了嗎,當我們在DateInterval中增加了getBegin方法之后會是什么樣子的呢?是不是Peroid中有一個Object getBegin()的方法,而DateInterval中有一個Date getBegin()方法呢,這兩個方法在Java中是不能同時存在的,可是Java5以后增加了一個協變類型,使得這里是被允許的,看看DateInterval中getBegin方法就知道了:

@Override  
public Date getBegin(){ return super.getBegin(); }  

這里用了@Override,說明是覆蓋了父類的Object getBegin()方法,而返回值可以指定為父類中的返回值類型的子類,這就是協變類型,這是Java5以后才可以允許的,允許子類覆蓋了方法后指定一個更嚴格的類型(子類型)。

總結:

  • 1.記住一點,虛擬機中沒有泛型,只有普通的類。
  • 2.所有泛型的類型參數都用它們限定的類型代替,沒有限定則用Object。
  • 3.為了保持類型安全性,虛擬機在有必要時插入強制類型轉換。
  • 4.橋方法的合成用來保持多態性。
  • 5.協變類型允許子類覆蓋方法后返回一個更嚴格的類型。

六、約束和局限性

1.不能使用基本類型實例化泛型

不能使用基本類型作為類型參數,因為擦除之后,可能會是Object類型,Object類型是無法存儲基本類型的

2.運行時類型檢查只適用于原始類型

  • 使用getClass會返回一個原始類型,比如Object;
  • 使用instanceof和強制轉換都會出現錯誤和警告。
if(a instanceof Pari<String>) //Error
Pair<String> p = (Pair<String>) a;//Error

Pair<Employee> employee = ...
Pair<String> stringPari = ...
if(employee.getClasss()==stringPari.getClass())//返回true

3.不能創建參數化類型數組

不能實例化參數化類型數組,可以聲明變量:Pari<String>[] table,只是不能new,這樣做是為了保證數組的安全,因為在類型擦除的時候會變為Object,防止數組可以add任何元素進去。

Pair<String>[] table = new Pair<String>[10];//Error

4.不能實例化類型變量

類型擦除會將T修改為Object,而new Object()是不被允許的,可以通過反射來實例化一個泛型對象。

public Pair(){
    first = new T();
    second = new T();
}

5.不能構造泛型數組

因為類型擦除,不允許實例化一個泛型數組,防止add的時候出現ArrayStoreException。

public static <T extends Comparable >T[] minmax(T[] a){ //Error
    T[] mm = new T[2];
    ...
}

如果想實例化泛型數組,可以通過以下方法來解決:

  • 通過反射來解決
public static <T extends Comparable> T[] minmax(T ... t){
    T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType(),2);
}

七、通配符類型

通配符類型中,允許參數類型變化,前面的 ? extends X,可以讓編譯器知道只需要某個X的子類型,拒絕傳遞其他特定類型。

1.通配符超類型限定

表示類型的下界,格式是:? super X。

特點:

1、限定為X和X的超類型,直至Object類,因為不知道具體是哪個超類型,因此方法返回的類型只能賦給Object。

2、因為可以向上轉型,所以作為方法的參數時,可以傳遞X以及X的超類型。

3、作為方法的參數時,可以傳遞null。

作用:主要用來安全地寫入數據,可以寫入X及其超類型。

/** 
 * ICE 
 * 2016/10/17 0017 14:12 
 */  
public class Demo {  
    public static void main(String[] args) {  
        A a = new A();  
        B b = new B();  
        C c = new C();  
  
        D<? super A> d = new D<>();  
        Object o = d.get();  
  
        d.set(a);  
        d.set(b);  
        d.set(c);  
        d.set(null);  
    }  
}  
  
class A {  
    @Override  
    public String toString() {  
        return "A{}";  
    }  
}  
  
class B extends A {  
    @Override  
    public String toString() {  
        return "B{}";  
    }  
}  
  
class C extends A {  
    @Override  
    public String toString() {  
        return "C{}";  
    }  
}  
  
class D<T> {  
    public void set(T t) {  
    }  
  
    public T get() {  
        return null;  
    }  
} 

2.無限制

無限定不等于可以傳任何值,相反,作為方法的參數時,只能傳遞null,作為方法的返回時,只能賦給Object。

public class Demo {  
    public static void main(String[] args) {  
        D<?> d = new D<>();  
        Object o = d.get();  
        d.set(null);  
    }  
}  
  
class A {  
    @Override  
    public String toString() {  
        return "A{}";  
    }  
}  
  
class B extends A {  
    @Override  
    public String toString() {  
        return "B{}";  
    }  
}  
  
class C extends A {  
    @Override  
    public String toString() {  
        return "C{}";  
    }  
}  
  
class D<T> {  
    public void set(T t) {  
    }  
  
    public T get() {  
        return null;  
    }  
}  

有什么作用呢?對于一些簡單的操作比如不需要實際類型的方法,就顯得比泛型方法簡潔,可以這樣說:如果是“讀”操作 則需要限定 上邊界,如果是寫操作則需要限定下邊界;而無限定通配符表示只讀,不能進行增加、修改。

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

推薦閱讀更多精彩內容

  • 引言:泛型一直是困擾自己的一個難題,但是泛型有時一個面試時老生常談的問題;今天作者就通過查閱相關資料簡單談談自己對...
    cp_insist閱讀 1,855評論 0 4
  • 泛型是Java 1.5引入的新特性。泛型的本質是參數化類型,這種參數類型可以用在類、變量、接口和方法的創建中,分別...
    何時不晚閱讀 3,047評論 0 2
  • object 變量可指向任何類的實例,這讓你能夠創建可對任何數據類型進程處理的類。然而,這種方法存在幾個嚴重的問題...
    CarlDonitz閱讀 926評論 0 5
  • Why ——引入泛型機制的原因 假如我們想要實現一個String數組,并且要求它可以動態改變大小,這時我們都會想到...
    absfree閱讀 5,133評論 1 6
  • 泛型的好處 使用泛型的好處我覺得有兩點:1:類型安全 2:減少類型強轉 下面通過一個例子說明: 假設有一個Tes...
    德彪閱讀 1,134評論 0 0