java 泛型程式設計

Fysddsw_lc發表於2018-03-30

泛型介紹

泛型程式設計 :可以被很多不同型別的物件所重用。比那些直接使用Object變數,然後強制型別轉換的程式碼具有跟好的安全性和可讀性。

使用型別引數可以將需要使用的型別,提前宣告

ArrayList<String> newlist = new ArrayList<String>();複製程式碼

使用型別引數可以告知這個類適用於什麼型別,當呼叫對應的get()方法的時候,不需要進行強制型別轉換,編譯器本事就知道其對應的型別。

當實現一個泛型的時候非常不容易,因為你需要知道這個這個類對應的所有用途及其型別,所以java提供了萬用字元型別,來解決這個問題。

定義簡單泛型類

型別變數使用大寫形式,且比較短,這是很常見的。在java庫中,使用變數E表示集合的元素型別,K和V分別表示表的關鍵字和值的型別。T(需要時還可以用臨近的字母U和S)表示型別。

泛型類,就是指具有一個或者多個型別變數,也就是說這個類適應這幾種型別,對於之後在那類來說,我們只關注泛型,而不會為資料村吃的細節煩惱。

使用型別變數T,用<>括起來,放在類名後面。這個泛型可以有多個型別變數,如<T,U>

可以使用類定義的型別變數指定類中屬性和方法的型別。

public class Pari<T> {  
  
    private T first;  
    private T second;  
      
    public Pari(){  
        first = null;  
        second = null;  
    }  
    public Pari(T first,T second){  
        this.first = first;  
        this.second = second;  
    }  
      
    public T getFirst(){  
        return first;  
    }  
      
    public T getSecond(){  
        return second;  
    }  
      
    public void setFirst(T value){  
        first = value;  
    }  
    public void setSecond(T value){  
        second = value;  
    }  
} 
複製程式碼

 Pair類引入了一個型別變數T,用尖括號(<>) 括起來, 並放在類名的後面;
泛型類可以有多個型別變數, 如, 定義 Pair 類, 其中第一個和第二個域使用不同的型別

其實泛型類可以看做是普通類的工廠。

泛型方法

泛型方法既可以在普通類中,也可以在泛型類中,定義方式是在方法名前加<T> T,說明該方法是泛型方法

class ArrayAlg
{
    public static<T> T getMiddle(T... a)
    {
        return a[a.length / 2];
    }
}
複製程式碼

當呼叫一個泛型方法時, 在方法名前的尖括號中放入具體的型別

String middle = ArrayAlg.<String>getMiddle("john", "Q.", public ");複製程式碼

方法呼叫中可以省略 型別引數, 編譯器有足夠的資訊能偶推斷出所呼叫的方法;

也就是說, 可以呼叫

String middle = ArrayAlg.getMiddle("john", "Q.", public ");複製程式碼

 對於泛型方法的引用都沒有問題。 偶爾, 編譯器也會提示錯誤,

double middle = ArrayAlg.getMiddle(3.14, 0, 1729);1複製程式碼
編譯器將會自動打包引數為 1個 Double 和 2個Integer 物件,而後尋找這些類的共同超型別。
事實上, 找到2個這樣的超型別:Number 和 Comparable 介面, 其本身也是一個泛型型別。 在這種情況下, 可以採取的補救措施是 將 所有的引數寫為 double 值;

型別變數的限定

有的時候,比如對於特定的方法執行特定的操作,但是該操作不適用於一些型別,這時可以對型別變數T設定限定,可以使其整合特別的類或者介面(沒錯,在這裡對於介面也是使用繼承,因為使用extends更加接近子類的意思)

一個型別變數或萬用字元可以有多個限定,限定型別用“”&“” 分隔,而用逗號用來分隔型別變數。在java繼承中,可以根據需要擁有多個介面超型別,但限定中至多有一個類。如果用一個類作為限定,但必須是第一個。

比如:T extends Comparable & Srializable

public static <T extends Comparable> Pari<T> getMinMax(T[] word){  
      
    if(word == null || word.length == 0)  
        return null;  
    T min = word[0];  
    T max = word[0];  
    for(int i=1;i<word.length;i++){  
        if(word[i].compareTo(max) > 0)  
            max = word[i];  
        if(word[i].compareTo(min) < 0)  
            min = word[i];  
    }  
    return new Pari<T>(min,max);  
}
複製程式碼

JVM中沒有泛型,只有普通的類和方法

所有的型別引數都是用他們的限定型別轉換(如果沒有型別引數,則使用Object型別),這個過程稱為擦除(erased),擦除型別變數,並替換為限定型別

有時為保持型別安全性,需要插入強制型別轉換

約束與侷限性

不能用基本型別例項化型別引數

不能用型別引數來代替基本型別。就是沒有Pair<double>,只有Pair<Double>。當然主要是原因是型別擦除。擦除之後,Pair類含有Obkect型別的域,而Object不能儲存double的值。

執行時型別查詢只適用於原始型別

虛擬機器中的物件總有一個特定的非泛型型別。因此,所有型別查詢只產生原始型別。

如:if(a instanceof Pari<String>)是錯誤的,因為只能查詢原始型別,即Pari,if(a instanceof Pari<T>)是錯誤的

又如:

Pari<String> pari1 = (Pari<String>) a

無論何時使用instanceod或者設計泛型型別的強制型別轉換表示式都會看到一個編譯器警告。

同樣道理,getClass方法總是返回原始型別。

if (pari1.getClass() == pari2.getClass())返回true,因為兩次getClass()都是返回Pari.class

不能建立引數化型別的陣列

Couple<Employee>[] couple = new Couple<Employee>[5] ;這種宣告式不合法的,這裡面有一個問題還是通過型別擦除機制來解釋,型別擦除後couple的型別是Couple[],考慮一下兩種賦值方式:
1.couple[0] = "wife"時,編譯器會報錯,這個很明顯的錯誤,couple每個元素都是Couple型別。
2.couple[0] = new Couple<String>(),型別擦除後這個可以通過陣列檢測,但仍然是錯誤的型別,因為couple在宣告的時候定義的是Couple<Employee>,所以會出現問題。
如果要存放引數化型別物件的集合,可以考慮使用ArrayList<Couple<Employee>>進行宣告,而且Java中建議優先使用集合,這樣既安全又有效。

不能丟擲也不能捕獲泛型類例項

 在Java中,public class GenericException <T> extends Exception {...}這種泛型類擴充套件子Throwable是不合法的,不能通過編譯器。
不能再catch子句中使用型別引數,如:
public static <T extends Throwable> void doWork(Class<T> t) {  
     try {  
           // do work...  
     } catch (T e) {  
           e.printStackTrace();  
     }  
  }  // 錯誤 複製程式碼

而這個是合法的

public static <T extends Throwable> void doWork(T t) throws T {  
          try {  
                    // do work...  
         } catch (Throwable e) {  
                   e.printStackTrace();  
                    throw t;  
         }  
}// 正確  複製程式碼

java 異常處理的一個基本原則是,必須為所有已檢查異常提供一個處理器。不過可以利用泛型消除這個限制。

不能例項化型別變數 

不能使用像new<T>(...),new t[...]或T.class這樣的表示式中的型別變數。例如,下面的Pair<T>構造器就是非法的:

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

複製程式碼

型別擦除將T改變成Object。而且本意肯定不希望呼叫newObject(),但是可以通過反射呼叫Class.newInstance方法來構造泛型物件。

遺憾的是T.class在Java中也是不被支援使用的,所以一種彌補的方式,傳入一個型別闡述為T的Class物件,如Class<T>

 public static <T> Couple<T> createInstance(Class<T> clazz) {  
          try {  
                    return new Couple<T>(clazz.newInstance(), clazz.newInstance());  
         } catch (Exception e) {  
                    return null ;  
         }  
}  
複製程式碼

     初學者對Java反射不熟悉不用著急,這裡只要知道不能例項化型別引數即可,同理,不能例項化一個泛型陣列,如 

public static <T> T[] maxTwo(T[] values) {
    T[] array = new T[2];
} // 錯誤   

複製程式碼

     泛型構建陣列是不合法的,因為這麼構建在擦除之後構造的永遠是new Object[2],這不是我們所希望的結果。而且這樣會導致一些執行時錯誤。為了更進一步說明隱患問題,來看看下面程式碼: 

public static <T extends Comparable<T>> T[] maxTwo(T[] array) {  
     Object[] result = new Object[2];  
     return (T[]) result; // Type safety: Unchecked cast from Object[] to T[]  
}  
複製程式碼

     這種方式會產生變異警告:Object[]轉換為T[]是沒有被檢查的。我們來試一試這個呼叫: maxTwo(new String[] { "5", "7" , "9" });,執行後,發生了型別轉換異常,因為方法在呼叫的時候將Object[]轉換為String[],失敗的型別轉化。怎麼解決呢?同樣這裡可以使用Java發射來解決: 

public static <T extends Comparable<T>> T[] maxTwo(T[] array) {  
     // Type safety: Unchecked cast from Object[] to T[]  
     return (T[]) Array.newInstance(array.getClass().getComponentType(), 2) ;  
}  
複製程式碼

泛型類的靜態上下文中型別變數無效 

不能在靜態域或方法中引用型別變數。

public class Singleton<T>
{
    public static T singleInstacne;//ERROR
    public static T getSingleInstance();//ERROR
    {
        if(singleInstacne==null)
        return singleInstance;
    }
}複製程式碼

如果這個程式能夠執行,就可以宣告一個Singleton<Random>共享隨機數生成器,宣告一個Singleton<JFileChooser>共享檔案選擇器對話方塊。但是,這個程式無法工作。型別擦除之後,只剩下Singleton類,他只包含一個singleInstance域。

注意擦除後的衝突

public class NameClash<T> {  
      public boolean equals(T value) {  
               return false ;  
     }  
}  
複製程式碼

     從這個類的定義中來看,存在兩個equals方法,一個是自身定義的public boolean equals(T value) {...},一個是從Object繼承的public boolean equals(Object obj) {...},但型別擦除以後,前者方法成為了public boolean equals(Object value) {...},而在一個類中同時存在兩個方法名和引數一樣的方法是不可能的,所以這裡引發的衝突是沒法通過編譯器的。可以通過重新命名方法進行修正。
擦除引起的衝突還體現在另一點上,再看一段錯誤的程式碼:

class Calendar implements Comparable<Calendar> {...}  
class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar> {...}  複製程式碼
上述程式碼是非法的,為什麼?回顧一下型別擦除後,虛擬機器會為
Calendar
類合成橋方法,實現了Comparable<Calendar>獲得一個橋方法:
public int compareTo (Object o) {return compareTo((Calendar)o);}
而實現了Comparable<GregorianCalendar >在型別擦除後,虛擬機器為GregorianCalendar合成一個橋方法:
public int compareTo (Object o) {return compareTo((GregorianCalendar )o);}
這樣一來在GregorianCalendar類中存在兩個一樣的方法,這是不允許的。



相關文章