1. 程式人生 > >Java的泛型約束和限制

Java的泛型約束和限制

實例 == -h 不同 java異常 轉換 component 參數 測試

不能用基本類型實例化類型參數


不能用類型參數代替基本類型:例如,沒有Pair<double>,只有Pair<Double>,其原因是類型擦除。擦除之後,Pair類含有Object類型的域,而Object不能存儲double值。這體現了Java語言中基本類型的獨立狀態。

運行時類型查詢只適用於原始類型(raw type)


運行時:通常指在Classloader裝載之後,JVM執行之時

類型查詢:instanceof、getClass、強制類型轉換

原始類型:即(raw type),泛型類型經編譯器類型擦除後是Object或泛型參數的限定類型(例如Pair<T extends Comparable>,Comparable就是T的限定類型,轉化後泛型的原始類型就是Comparable,所以Pair類不帶泛型是Pair<Comparable>),即Pair類含有Comparable類型的域

JVM中沒有泛型

eg:

技術分享
if(a instanceof Pair<String>) //ERROR,僅測試了a是否是任意類型的一個Pair,會看到編譯器ERROR警告


if(a instanceof Pair<T>) //ERROR


Pair<String> p = (Pair<String>) a;//WARNING,僅測試a是否是一個Pair


Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if(stringPair.getClass() == employeePair.getClass())  //會得到true,因為兩次調用getClass都將返回Pair.class
技術分享

不能創建參數化類型的數組(泛型數組)


參數化類型的數組:指類型帶有泛型參數的數組,也即泛型數組,如Pair<T>[] 、 T[]

  不能實例化參數化類型的數組,例如:

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

  在這裏我們假設可以實例化,那麽經編譯器類型擦除後,table的類型是Pair[],我們再讓它協變為Object[]:

Object[] objArray = table;

  而一般來說,數組會記住他的元素類型Pair,我們如果試圖存儲其他類型的元素

,就會拋出異常(數組存儲檢查),例如:

objArray[0] = "Hello"; //ERROR--component type is Pair

  但是,對於泛型類型Pair<String>,類型擦除會使這種不同類檢查機制無效,這就是不能實例化泛型數組的原因

objArray[0] = new Pair<Employee>();  //如果泛型機制允許我們實例化數組,那麽這一步就沒理由出錯了!而這違背了我們的初衷(限定類型)
  • 數組存儲只會檢查擦除後的類型,又因為Java語言設計數組可以協變,所以可以通過編譯
  • 能夠通過數組存儲檢查,不過仍會導致一個類型錯誤,故不允許創建參數化類型的數組
  • 註意,聲明類型為Pair<String>[]的變量是合法的,只是不能創建這些實例(我們應該直接用new Pair<String>[10]{......}來初始化這個變量)

泛型數組的間接實現

通過泛型數組包裝器,如ArrayList類,維護一個Object數組,然後通過進出口方法set、get來限定類型和強制轉換數組類型,從而間接實現泛型數組,

例如:ArrayList: ArrayList<Pair<T>>、ArrayList<T>

不能實例化類型變量T


  • 即不能使用new T(..) , new T[..] 或 T.class 這樣的表達式中的類型變量
    • 例如: public Pair() { first = new T(); } //ERROR! 類型擦除將T改變成Object,調用非本意的new Object()
  • 不能使用new T(..)
    • 但是,可通過反射調用Class.newInstance方法來構造泛型對象(要註意表達式T.class是非法的)
    • 技術分享
      public static <T> Pair<T> makePair(Class<T> cl){
          try{ return new Pair<>(cl.newInstance() , cl.newInstance()); }
          catch(Exception ex) { return null; }
      }
      
      //這個方法可以按照下列方式調用:
      Pair<String> p = Pair.makePair(String.class);  
      技術分享
    • 註意:Class類本身是泛型。String.class是一個Class<String>的實例,因此makePair方法能夠推斷出pair的類型
  • 不能使用new T[..]
    • 即不能(直接)創建泛型數組,參考我另一篇博文(http://www.cnblogs.com/ixenos/p/5648519.html),這裏不再贅述
    • 解決方案:使用泛型數組包裝器,例如ArrayList
      • 然而,當在設計一個泛型數組包裝器時,例如方法minmax返回一個T[]數組,則泛型數組包裝器無法施展,因為類型擦除,return (T [])new Object是沒有意義的強轉不了。此時只好利用反射,調用Array.newInstance:
      • 技術分享
        import java.lang.reflect.*;
        ...
        public static <T extends Comparable> T[] minmax(T... a){
            T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType() , 2);
        ...
        }
        技術分享
        • 【API文檔描述】public Class<?> getComponentType() 返回表示數組組件類型的 Class。如果此類不表示數組類,則此方法返回 null。

    • 而ArrayList類中的toArray方法的實現就麻煩了
      • public Object[] toArray() 無參,返回Object[]數組即可 
        public Object[] toArray() {
                return Arrays.copyOf(elementData, size);
            }
        • 【API文檔描述】public static <T> T[] copyOf(T[] original,int newLength)
            復制指定的數組,截取或用 null 填充(如有必要),以使副本具有指定的長度。對於在原數組和副本中都有效的所有索引,這兩個數組將包含相同的值。對於在副本中有效而在原數組無效的所有索引,副本將包含 null。當且僅當指定長度大於原數組的長度時,這些索引存在。所得數組和原數組屬於完全相同的類。
      • public <T> T[] toArray(T[] a) a - 要存儲列表元素的T[]數組(如果它足夠大)否則分配一個具有相同運行時類型的新數組,返回該T[]數組
      • 技術分享
        @SuppressWarnings("unchecked")
            public <T> T[] toArray(T[] a) {
                if (a.length < size)
                    // Make a new array of a‘s runtime type, but my contents:
                    return (T[]) Arrays.copyOf(elementData, size, a.getClass()); //a.getClass()得運行時目的數組的運行時類型
                System.arraycopy(elementData, 0, a, 0, size);
                if (a.length > size)
                    a[size] = null;
                return a;
            }
        技術分享
        •  【API文檔描述】
          public static <T,U> T[] copyOf(U[] original,int newLength, Class<? extends T[]> newType)
          復制指定的數組,截取或用 null 填充(如有必要),以使副本具有指定的長度。對於在原數組和副本中都有效的所有索引,這兩個數組將包含相同的值。對於在副本中有效而在原數組無效的所有索引,副本將包含 null。當且僅當指定長度大於原數組的長度時,這些索引存在。所得數組屬於 newType 類。

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


  • 泛型類不能在靜態域或靜態方法中引用類型變量
  • public class Singleton<T>{
        private static T singleInstance; //ERROR
        public static T getSingleInstance(){...} //ERROR
    }
    • 類型擦除後只剩下Singleton類,因為靜態所以他只包含一個singleInstance域,如果能運行則以Singleton類為模板生成不同類型的域,因此產生了沖突

不能throws或catch泛型類的實例(有關異常)


  • 泛型類繼承Throwable類不合法,如public class Problem<T> extends Exception {...} //ERROR 不能通過編譯
  • catch子句不能使用類型變量
  • 技術分享
    public static <T extends Throwable> void doWork(Class<T> t){
        try{
                do work
            }catch (T e){ // ERROR
                Logger.global.info(...)
            }
    }
    技術分享
  • 不過,在異常規範中使用類型變量是允許的:  
  • 技術分享
    public static <T extends Throwable> void doWork(T t) throws T { //此時可以throws T
        try{
                do work
            }catch (Throwable realCause){ //捕獲到具體實例
                t.initCause(realCause); 
                throw t; //這時候拋具體實例,所以throw t 和 throws T 是可以的!
            }
    }
    技術分享
    • 此特性作用:可以利用泛型類、類型擦除、SuppressWarnings標註,來消除對已檢查(checked)異常的檢查,
      • unchecked和checked異常: Java語言規範將派生於Error類或RuntimeException的所有異常稱為未檢查(unchecked)異常,其他的是已檢查(checked)異常
      • Java異常處理原則:必須為所有已檢查(checked)異常提供一個處理器,即一對一個,多對多個 技術分享
        @SuppressWarnings("unchecked")  //SuppressWarning標註很關鍵,使得編譯器認為T是unchecked異常從而不強迫為每一個異常提供處理器
        public static <T extends Throwable> void throwAs(Throwable e) throws T{  //因為泛型類型擦除,可以傳遞任意checked異常,例如RuntimeException類異常
            throw (T) e;
        }
        技術分享
        • 假設該方法放在類Block中,如果調用 Block.<RuntimeException>throwAs(t); 編譯器就會認為t是一個未檢查的異常
        • 技術分享
          public abstract class Block{
              public abstract void body() throws Exception;
              public Thread toThread(){
                  return new Thread(){
                                  public void run(){
                                      try{
                                           body();
                                      }catch(Throwable t){
                                           Block.<RuntimeException>throwAs(t);
                                      }
                                  }
                              };
              }
          
              @SuppressWarnings("unchecked")
              public static <T extends Throwable> void throwAs(Throwable e) throws T{
              throw (T) e ;
              }
          }
          技術分享
        • 再寫個測試類
        • 技術分享
          public class Test{
              public static void main(String[] args){
                  new Block(){
                      public void body() throws Exception{
                          //不存在ixenos文件將產生IOException,checked異常!
                          Scanner in = new Scanner(new File("ixenos"));
                          while(in.hasNext())
                              System.out.println(in.next());
                      }
                  }.toThread().start();
              }
          }    
          技術分享
          • 啟動線程後,throwAs方法將捕獲線程run方法所有checked異常,“處理”成unchecked Exception(其實只是騙了編譯器)後拋出;
          • 有什麽意義?正常情況下,因為run()方法聲明為不拋出任何checked異常,所以必須捕獲所有checked異常並“包裝”到未檢查的異常中;意義:而我們這樣處理後,就不必去捕獲所有並包裝到unchecked異常中,我們只是拋出異常並“哄騙”了編譯器而已

註意擦除後的沖突


  • Java泛型規範有個原則:“要想支持擦除的轉換,就需要強行限制一個泛型類或類型變量T不能同時成為兩個接口類型的子類而這兩個接口是統一接口的不同參數化
    • 註意:非泛型類可以同時實現同一接口,畢竟沒有泛型,很好處理
    • class Calender implements Comparable<Calender>{...}
      
      class GGCalender extends Calender implements Comparable<GGCalender>{...} //ERROR
      • 在這裏GGCalender類會同時實現Comparable<Calender> 和 Comparable<GGCalender>,這是同一接口的不同參數化

Java的泛型約束和限制