淺談Java中的泛型

引言:泛型一直是困擾自己的一個難題,但是泛型有時一個面試時老生常談的問題;今天作者就通過查閱相關資料簡單談談自己對泛型的理解;

一:什么是泛型:

  • 泛型就是參數化類型,就是所操作的數據類型被指定為一個參數,這種類型可以在類接口和方法中創建,分別稱之為泛型類,泛型接口,泛型方法

二:為什么要使用泛型:

  • 使用泛型可以讓我們編寫的代碼可以被很多類型的對象重用;
    例如我們不希望為String和file對象分別設計不同的類;實際上我們也并不需要這么做;
    就拿ArrayList類舉例:可以聚集任何類型的對象;Javase5.0之前是使用繼承實現的;ArrayList類只維護一個Object引用的數組;
Public class ArrayList{
    Public Object get(int i){
    }
  Public void add(Object o){
  }
  Public Object[] elementData;
  }

問題1:獲取值必須進行強制類型轉換;
問題2:沒有錯誤檢查可以向數組列表中添加任何參數;
針對以上兩種情況泛型提出了更好的解決方案;類型參數;(type parameters)使我們設計的程序具有更好的可讀性和安全性

三:泛型都有哪些?

  • 1:泛型類:

    就是一個或者多個具有類型變量的類;如下:
 public class Pair<T>{
    private T first;
    private T second;  
    public Pair(){
      first = null;
      second = null;
    }
    public void setFirst(T first){
      this.first =   first;
    }
    public void setSecond(T second){
      this.second = second
    }
    public T getFirst(){
        return this.first;
    }
    public T getSecond(){
        return Second;
    }
  }

Pair類引入了一個類型變量T,將其放在"<>"里面;并且將其放在類名的后面,泛型類也可以有多個類型變量;
可以將泛型類看做是普通類的工廠;

  • 2:泛型方法:

    帶有類型參數的簡單方法;
    泛型方法可以定義在普通類里面也可以定義在泛型類里面;
    所謂的泛型方法要么就是返回值是一個泛型,要么就是參數是泛型;
    class ArrayAlg{
      public static <T> T getMiddle(T[] a){
        return a[a.length/2]
      }
    }
    
  • 3:泛型接口

    就是一個或者多個具有類型變量的接口
    pulbic interface Collection<E> extends Iterable<E> {
    Iterator<E> iterator();
    }

四:泛型使用方法

  • 1:泛型代碼和虛擬機

    注意虛擬機中沒有泛型類型的對象,所有的對象都屬于普通類;
    無論什么時候定義泛型,java虛擬機都會自動提供一個相應的原始類型(raw type);原始類型的名字就是刪除泛型參數之后的泛型類型名;擦除類型變量,并替換為限定類型,如果沒有限定類型的變量就替換成Object;
    如上面的Pair:替換之后的代碼為:
 public class Pair{
    private Object first;
    private Object second;  
    public Pair(){
      first = null;
      second = null;
    }
    public void setFirst(Object first){
      this.first =   first;
    }
    public void setSecond(Object second){
      this.second = second
    }
    public Object getFirst(){
        return this.first;
    }
    public Object getSecond(){
        return Second;
    }
  }

如果泛型類使用了限定類型如下:

public class Interval<T extends Comparable & Serializabler>{
  public Interval(T first,T second){
    if(first.comparaTo(second)<=0){
      lower - first;upper = second;
    }
    ....
    private T lower;
    private T upper;
  }
}
//轉換成原始類型之后
public class Interval extends erializabler{
  public Interval(Comparable first,Comparable second){
    if(first.comparaTo(second)<=0){
      lower - first;upper = second;
    }
    ....
    private Comparable lower;
    private Comparable upper;
  }
}
<1>:翻譯泛型表達式
當程序調用泛型方法時,如果擦除返回類型,編譯器就會插入強制類型轉換;eg:
  Pair<Employee> buddies = ...;
  Employee buudy = buudies.getFirst();
  //這里擦書getFirst的返回類型后將返回Object類型。編譯器自動插入Employee的強制類型轉換。也就是說編譯器把這個方法翻譯為兩條虛擬機指令:
  A:對原始方法Pair.getFirst的調用
  B:將返回的Object類型強制轉換為Employee類型
<2>:翻譯泛型方法
public static <T extends Comparable> T min(T[] a)是一個完整的方法族,擦除類型之后只剩下一個方法了:

 public static Comparable min(Comparable[] a)
 此時僅僅留下限定類型Comparable
eg: class DateInterval extends Pair<Date>{
         public void setSecond(Date second){
             if(second.compareTo(getFirst())>=0){
                 super.setSecond(second); 
             }
         }
     }
//類型擦出之后:
class DateInterval extends Pair{
         public void setSecond(Date second){
             if(second.compareTo(getFirst())>=0){
                 super.setSecond(second); 
             }
         }
     }

此時存在另一個從Pair中繼承來的setSecond方法,即
public void setSecond(Object second)
這個方法顯然和setSecond(Date date)不是同一個方法;
考慮下面的代碼:

DateInterval interval = new DateInterval(...);
 Pair<Date> pair = interval;
 pair.setSecond(aDate);

這里我們希望setSecond的調用具有多態性;并調用最合適的那個方法;希望pair調用DateInterval.setSecond方法;但此時類型擦書和多態已經發生了沖突:想要解決這個問題我們需要建立一個橋方法;

public void setSecond(Object second){
  setSecond((Date) second)
}

總之:記住有關java泛型轉換的事實:
A:虛擬機中沒有泛型,只有普通方法和普通類;
B:所有的類型參數都用他們的限定類型替換
C:橋方法被合成用來保持多態;
D:為保持類型安全性,必要時插入強制類型轉換。

2:約束和局限性:

使用java中的泛型時需要考慮一些限制,大多是限制都是由于類型擦除引起的;

<1>:不能用基本類型實例化類型參數
沒有Pair<double>只有Pair<Double>
<2>:運行時類查詢只適用于原始類型
虛擬機中的對象總有一個特定的非泛型類型。因此所有的類型查詢只產生原始類型。例如:
if(a instanceof Pair<String>)//ERROR
<3>:不能創建參數化類型的數組:

Pair<String>[] table = new Pair<String>[10]//ERROR;
但是聲明類型Pair<String>[]的變量仍是合法的;

<4>:Varargs警告:

由于java布置池泛型類型的數組;當我們向一個參數個數可變的方法 傳遞一個泛型類型的實例:
eg:

public static <T> void addAll(Collection<T> coll,T...ts){
  for(T t:ts)coll.add(t);
}
實際上ts是一個數組,包含提供的所有實參。
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ....;
Pair<String> pair2 = ....;
Pair<String> pair3 = ....;
addAll(table,pair1,pair2);
為了調用這個方法,Java虛擬機必須建立一個Pair<String>數組;違反前面的規定;但是這種情況并不會報告錯誤僅僅會報告一個警告可以通過兩種方式去消除它:
A:為addAll方法添加標注@SuppressWarning("unchecked").
B:@SafeVarargs直接標志addAll方法
@SafeVarags
public static <T> void addAll(Collection<T> coll,T...ts)
#####<5>:不能實例化類型參數
new T(...),new T[]或者T.class都是不允許的;
但是可以通過反射加上一定的API便可實現泛型的實例化;
```java
  public static <T> Pair<T> makePair(Class<T> cl){
      try{
          return new Pair<>(cl.newIntance(),cl.newInstance())
      }catch(Exception e){
        return null;
      }
  }
Pair<String> p = Pair.makePair(String.class)

類型擦除會讓這個方法永遠構造Object[2]數組;
如果數組僅僅作為一個類的私有實例域,就可以將這個數組聲明為Object[],并在獲取元素時進行類型轉換。例如ArrayList就是這樣實現的;

Public class ArrayList<E>{
  Private Object[] elements;
  @SuppressWarning(‘unchecked’)
  public E get(int n){
      return (E) elements[n]}
   }
  Public void set(int n,E e){elements[n] = e};
  Public class ArrayList<E>{
       Private E[] elements;
       Public ArrayList(){
       Elements = (E[])new Object[10];//假像:類型擦除會使其無法差距
  }
}

編譯時不會報錯,但是當我們的程序執行時,當我們把Object[]引用賦值給T[]時就會報錯;將會發生ClassCastException異常;
這種情況下我們可以利用反射:

Public static <T extends Comparable> T[] minmax(T…a){
  T[] mm = (T[])Array.newInstance(a.getClass().getComponentType(),2)
}
<6>泛型類的靜態上下文中類型變量無效

不能在靜態域或方法中引用類型變量(即泛型);(即被static修飾的域或者方法)

<7>:不能拋出或者捕獲泛型類的實例

既不能拋出也不能捕獲泛型類的實例對象。實際上,甚至泛型類擴展Throwable都是不合法的;T extends Exception(Throwable)
Catch字句中不能使用類型變量;catch(T e)Error;
PS:可以消除已檢查異常的檢查:
Java異常處理的一個基本原則:必須為所有的已檢查異常提供一個處理器;不過可以利用泛型消除這個限制;
當你必須捕獲run中所有的已檢查異常,將其包裝到未檢查異常中,因為run方法聲明為不拋出任何已檢查異常;
不過在這里我么你沒有選擇這種“包裝”我們只是拋出異常,并“哄騙瀏覽器”讓它認為這不會一個已檢查異常;
通過使用泛型類,擦除,和@SuppertWarning標注就可以消除java類型系統的部分限制

<8>注意擦除后的沖突:

當泛型類型被擦除時,無法創建引發沖突的條件,如:
在某個泛型類中添加equals()方法,當我們給這個泛型類中間穿具體的類型時:
進過類型擦除之后equals(String o)方法變為equals(Object o);
所以此時從概念上講:他有兩個equals方法:
Boolean equals(String)
Boolean equals(Object)和Object.Equals(Object)方法沖突;
補救方法就是重命名引發錯誤的方法
另外一個原則:“想要支持擦除的轉換,就需要強行限制一個類或類型變量不能同時成為兩個接口類型的子類”而這兩個接口是同一個接口的不同參數化;
例如下面的代碼就是非法的

Class Calendar implements Comparable<Calendar>{…}
Class GregorianCalendar extends Calendar implements Comparable< GregorianCalendar>(Error)
GregorianCalendar會事先Comparable<Calendar>和Comparable<GregorianCalendar>

這是同一接口的不同參數化;這一限制和類型擦柱的關系不是十分明確;下列的非泛型的版本就是合法的;
原因:有可能與合成的橋方法產生沖突。實現了Comparable<x>的類可以獲得一個橋方法;

Public int comparaTo(Object other){
  return compareTo(X) other
};

對于不同類型的;不能有兩個這樣的方法;

<9>泛型類型的繼承規則:
  • 例如:Employee和Manager。Pair<Manager>和Pair<Employee>之間沒有任何關系;
    無論S和T有什么關系,通常Pair<S>和Pair<T>之間都不會有什么聯系;
    繼承泛型類
  • 子類不是泛型類:需要給父類傳遞類型常量

    當給父類傳遞的類型常量為String時,那么在父類中所有T都會被String替換!

  • 子類是泛型類:可以給父類傳遞類型常量,也可以傳遞類型變量

3:通配符類型:

為了解決固定的泛型類型使用的不便利:java的設計者們發明了一種巧妙的(仍然是安全的)“解決方案”:通配符類型;

<1>:子類通配符:
   例如:Pair<? Extends Employee>
   表示任何泛型Pair類型,它的類型參數是Employee的子類;如Pair<Manager>

例子:
Pair<Manager> managerBuddies = new
Pair<>(ceo,cfo);
Pair<? Extends employee>
wildcardBuddies = managerBuddies;
wildcardBuddies.setFirst(lowlyEmployee);//error;
這可能不會以你破壞,對setFirst的調用有一個類型錯誤;
當你調用setFirst(? Extends Employee)編譯器僅僅知道是某個Employee的子類型,但是不知道具體是什么類型;它拒絕傳遞任何的類型。
但是使用getFirst就不存在這個問題;
這就是引入限定的統配符的關鍵之處;

<2>:通配符的超類型限定:
   通配符限定和類型變量限定十分類似;但是還有一個附加能力,即可以指定一個超類型限定(supertype bound)
   ? super Manager設置類型的下限;
   這個統配符限制為Manager的所有超類型;帶有超類型限定的通配符的行為和12.8接講的正好相反,可以為方法提供參數,但是不能使用返回值。
例如:
Pair<? Super Manager>
Void setFirst(? Super Manager)
? super Manager getFirst();

編譯器不知道setFirst方法的確切類型,但是可以調用任意Manager對象(或其子類型,例如Executive)調用它,而不能使用Employee對象調用它,然而,如果調用getFirst,返回的對象類型就不能的都保證;只能將它的值賦給Object;
直觀的講,帶有超類型的限定的通配符可以向泛型對象寫入,帶有子類型限定的通配符可以從泛型對象中讀取;

public void set(List<? extends Number> list){
        //list.add(new Integer(1));
        //可以獲取泛型參數類型的值,但是不能向里面添加值
        Number i =list.get(0);
        
    }
    public void set1(List<? super Integer> list){
        list.add(new Integer(1));
        //不可以獲取泛型參數類型的值,但是能向里面添加值
        //Integer i = list.get(0);
    }

超類型限定的另外一種應用:Comparable接口本身就是一個泛型類型;

Public interface Comparable<T>{
       Public intcompareTo(T other);
}

當有一個具體的類型傳入其中時,相應的泛型類型就會被自動轉換成傳入的類型;se5.0之前。Other是一個Object并且這個方法的實現需要強制類型轉換;

<3>:無限定的通配符:

還可以使用無限定的通配符;例如Pair<?>.

4:泛型和反射
<1>:使用Class<T>參數進行類型匹配
Public static <T> Pair<T> makePair(Class<T> c) throws
InstantiationException,IllegalAccessException{
Return new Pair<>(c.newInstance(),c.newInstance());
}

調用:makePair(Emloyee.class)

<2>虛擬機中的泛型類型信息:
   Java泛型的卓越特性之一就是在虛擬機中的類型擦除;雖然擦除了,但是在類中仍然會保留一些泛型祖先的微弱記憶;
   在java虛擬機中,需要重新構造是實現者聲明的泛型類,以及方法中的所有內容;(通過反射去獲得用戶傳遞的類型的一些信息)不會知道特定對象或者方法調用,以及如何解釋類型采參數;

為了表達泛型類型的聲明,java.lang.reflect包中提供了一個新的接口Type,這個接口包含了下列子類型的聲明:
Class類,描述具體的類型
TypeVariable接口
描述類型變量(如T extends Comparable<? Super T>)
WildcardType接口
描述通配符(如?Super T)
parameterizedType接口,描述泛型類或者接口類型(如Comparable <? Super T>)
GenericArrayType接口,描述泛型數組(如T[]);

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

推薦閱讀更多精彩內容