Java 泛型總結


1. 泛型類

class Gen<T> {
    private T t;
    
    public  T get(){
        return t;
    }
    
    public void set(T argt){
        t = argt;
    }
}

       “<>”內的T為類型參數,只能是類名,不能是基本類型(如int , double),泛型類(以及后面講到的泛型方法)可以有多個類型參數。

class Pair<K,V>{
    private K k;
    private V v;
    ……
}

       類型參數可以看做這個泛型類操作的數據類型

         泛類型的使用

Gen<String> gs = new Gen<String>();
gs.set("abc");
String str = gs.get();

2. 泛型方法

class GenFun{
    public <T> T Mid(T[] a){
        return a[a.length/2];
    }
}

        這是一個普通類,但是具有一個泛型方法,返回T類對象數組的中間位置的元素引用,泛型方法需要在返回值前用“<>”說明類型參數。

       (1)如果一個泛型類中有泛型方法,泛型方法的類型參數可以與泛型類的類型參數不同。

       (2)若在泛型類中的靜態方法要訪問泛型參數,必須使它變成泛型方法。

3.類型參數的限定

限定上限關鍵字 extends

class GenFun{
    public <T extends Comparable<T>> T Max(T[] a){
        T max = a[0];
        for(T t: a){
            if(t.compareTo(max) > 0){
                max = t;
            }
        }
        return max;
    }
}

         <T extends A> 這里的extends表示類型參數T必須就是A類或者A的子類,或者表示T需要實現A這個接口。上面的例子就表示T必須實現Comparable接口(注意Comparable接口本身又是個泛型接口)

現在我們來舉一個例子,來說明上限限定符的使用規則,假設E繼承 D, D繼承C, C繼承B, B繼承A

clip_image002

class A{};
class B extends A{};
class C extends B{};
class D extends C{};
class E extends D{};
class F{};

class Gen<T extends C> {
    private T t;
    
    public  T get(){
        return t;
    }
    
    public void set(T argt){
        t = argt;
    }
}

public class GenTest {
    public static void main(String[] args) {
        //----------------------
        Gen<F> gf = new Gen<F>();//編譯出錯 F不是C的子類
        Gen<B> ga = new Gen<B>();//編譯出錯 B是C的父類
        
        //----------------------
        Gen<C> gc = new Gen<C>();//編譯成功
        gc.set(new B());//編譯出錯 B不是C的子類

        Gen<D> gd = new Gen<D>();//編譯成功
        gd.set(new D());//編譯成功
        D d0 = gd.get();//編譯成功
        
        gd.set(new E());//編譯成功,把E的對象當做D對象看待
        D d1 = gd.get();//編譯成功,注意返回的類型為D
    }
}

有多個限定條件可以用“&”連接,多個限定條件中只能有一個類,其它的都為接口

限定下限關鍵字 super

class Gen<T super D> {//編譯錯誤
    ……
}

        限定條件可用來單獨限定方法(即泛型方法),也可以用來限定整個類。含有通配符的限定條件用於創建泛型類的引用。

4. 擦除

都是Object

         實際上java語言中並沒有泛類型對象,所有對象都是普通對象,也就說泛型僅僅存在於編譯階段。這一點我們可以從Gen<T>的字節碼中得到印證。查看字節碼我使用的是Bytecode visualizer插件,可以從http://marketplace.eclipse.org/下載到。

class Gen<T> {
    /* compiled from GenTest.java * private T t; Gen() { /* L18 */
        0 aload_0;                /* this */
        1 invokespecial 12;       /* java.lang.Object() */
        4 return;
    }

    public java.lang.Object get() {
        /* L22 */
        0 aload_0;                /* this */
        1 getfield 23;            /* .t */
        4 areturn;
    }

    public void set(java.lang.Object arg0) {
        /* L26 */
        0 aload_0;                /* this */
        1 aload_1;                /* argt */
        2 putfield 23;            /* .t */
        /* L27 */
        5 return;
    }
}

        注意get方法的返回值類型和set方法的參數類型都是Object。可以看出泛型類中,將泛型對象都用Object對象代替。由於get方法應該根據具體的類型參數返回一個對應類型的對象,那么這一點又是如何實現的。不妨先來觀察下面的代碼。

public class GenTest {
        public static void main(String[] args) {
        Gen<D> gd = new Gen<D>();
        gd.set(new D());
        D d0 = gd.get();
    }
}

         代碼的意思很簡單就是實例化一個Gen<D>的對象,並調用它的get和set方法。我們再來看看上述代碼對應的字節碼(省略了不相干的部分)。

public static void main(java.lang.String[] args) {
        /* L69 */
        0 new 16;
        3 dup;
        4 invokespecial 18;       /* javaleanning.Gen() */
        7 astore_1;               /* gd */
        /* L71 */
        8 aload_1;                /* gd */
        9 new 19;
        12 dup;
        13 invokespecial 21;      /* javaleanning.D() */
        16 invokevirtual 22;      /* void set(java.lang.Object d0) */
        /* L72 */
        19 aload_1;               /* gd */
        20 invokevirtual 26;      /* java.lang.Object get() */
        23 checkcast 19;          /* javaleanning.D */
        26 astore_2;              /* d0 */
        /* L106 */
        27 return;
}

         第20行是調用get方法,注意23行,它是進行強制類型轉換,26行是將結果傳遞給引用d0。也就是說泛型的實現原理就是在需要返回某個泛型對象之前,進行強制類型轉換。在使用泛型類中的方法或泛型方法時,會用實際的類名去替換T,這樣一來編譯器就知道了強制轉換的類型。還有一點要說明,強制轉換的代碼是編譯器在調用get方法時插入的,get方法中並沒有進行強制類型轉換(它也不知道該轉換成什么類型的對象),這樣可以使得泛型類和泛型方法的編譯不依賴於T的具體類型。

class Gen<T> {
    private T t;
    
    public  T get(){
        return t;
    }
    
    public  void set(T argt){
        t = argt;
    }
    
    public void set(Object argt){ //編譯出錯
        t = argt;
    }
}

        上述泛型類中添加一個public Object get() ,則會出現編譯錯誤,原因是在編譯時,set(T argt)會將泛類型擦除,變成set(Object argt),這樣一個類中就存在兩個完全相同的方法。

靜態數據公用

     因為擦除效應,類型參數不同的泛型類使用的是同一代碼和靜態數據。。我們在G<T>中添加一個靜態變量n

class Gen<T> {
    private T t;
    
    public static int n = 0;
    
    public  T get(){
        return t;
    }
    
    public  void set(T argt){
        t = argt;
    }
}

         現在我們做一個測試。

Gen<D> gd = new Gen<D>();
gd.n = 100;
Gen<E> ge = new Gen<E>();
System.out.println(ge.n);

         最后的輸出結果是100。

5. 覆蓋

            現在有一個SubGen類繼承了Gen<A>,並想覆蓋Gen<A>的set方法。

class SubGen extends Gen<A>{
    public void set(Object o){ //編譯出錯
        System.out.println("from SubGen set ");
    }
}

        雖然Gen<A>被擦除后的set方法的原型就是set(Object o),但是想覆蓋父類G<A>的set方法,上述寫法會出現編譯錯誤。因為編譯器以為它成功的欺騙了我們,讓我們以為存在泛類型,我們這么寫就是告訴它我們識破了騙局,編譯器必然不高興了,編譯就不能成功(真正的原因后面會介紹)。所以我們必須假裝不知道有擦除這回事,按照泛型的方式來處理。編譯器以為我們會認為Gen<A>的set方法的原型是set(A a),所以我們必須這么寫代碼才能實現子類對(泛型)父類中方法的覆蓋。

class SubGen extends Gen<A>{
    public void set(A a){
        System.out.println("from SubGen set");
    }
}

      但是現在還有個疑問,明明泛型中的類型參數編譯時都替換成了Object,那么子類中的set(A a)就不能覆蓋父類中的set(Object argt)方法了(因為這兩個方法的參數不同),但是我們運行下面的代碼卻能得到正確的結果(輸出 from SubGen set)。

Gen<A> ga = new SubGen();
ga.set(null);

        要解釋這個原因,我們可以查看SubGen的字節碼。

 public void set(javaleanning.A arg0) {
        /* L34 */
        0 getstatic 16;           /* java.lang.System.out */
        3 ldc 22;                 /* "from SubGen" */
        5 invokevirtual 24;       /* void println(java.lang.String arg0) */
        /* L35 */
        8 return;
    }

    /* bridge method generated by the compiler */
    public volatile void set(java.lang.Object arg0) {
        /* L1 */
        0 aload_0;
        1 aload_1;
        2 checkcast 33;           /* javaleanning.A */
        5 invokevirtual 35;       /* void set(javaleanning.A arg0) */
        8 return;
    }

         可以看到SubGen中有兩個set方法,public void set(javaleanning.A arg0)是我們自己編寫了,而另一個public volatile void set(java.lang.Object arg0) 是編譯器自動幫我們添加的(注意代碼的注釋bridge method generated by the compiler)。添加的這個方法正好和父類中擦除掉泛型的set方法同名且同參數public volatile void set(java.lang.Object arg0),這就實現了子類對父類方法的覆蓋,而set(java.lang.Object arg0)內部就是僅僅調用了我們寫的set(javaleanning.A arg0)方法。現在我們回頭看看前面我們編譯出錯的代碼,原因就顯而易見了。因為編譯器幫我們編寫了一個set(java.lang.Object arg0)方法,如果我們自己也實現一個set(Object o),那么兩個(另一個由編譯器自動生成)一樣的方法必然會產生沖突,所以會編譯失敗。

         現在SubGen中添加set方法想要覆蓋掉父類Gen<A>中的set方法。那么我們可以這么寫(代碼沒有什么具體意義,添加輸出語句,只是為了區別子類中的方法和父類中的方法)。

class SubGen extends Gen<A>{
    public void set(A a){
        System.out.println("from SubGen set");
    }
    
    public A get(){
        System.out.println("from SubGen get");
        return new A();
    }
}

         我們使用下面的代碼測試

Gen<A> ga = new SubGen();
ga.set(null);
ga.get();

            可以得到正確的結果

             from SubGen set

             from SubGen get

             我們查看字節碼可以發現一個有趣的問題

 public javaleanning.A get() {
        /* L38 */
        0 getstatic 16;           /* java.lang.System.out */
        3 ldc 22;                 /* "from SubGen" */
        5 invokevirtual 24;       /* void println(java.lang.String arg0) */
        /* L39 */
        8 new 34;
        11 dup;
        12 invokespecial 36;      /* javaleanning.A() */
        15 areturn;
    }

    /* bridge method generated by the compiler */
    public volatile java.lang.Object get() {
        /* L1 */
        0 aload_0;
        1 invokevirtual 38;       /* javaleanning.A get() */
        4 areturn;
    }

         SubGen 中有兩個get方法,它們僅僅返回值不同。一般情況下,程序員這樣寫代碼必然會導致編譯錯誤,但是由於是編譯器自己添加的一個方法(java.lang.Object get()),所以的確能夠編譯成功,並且編譯器能夠通過返回值的不同來區分這兩個方法。

6.泛型中的繼承

可以使用子類實例

           我們仍然假設E繼承 D, D繼承C, C繼承B, B繼承A。那么Gen<C> 中的方法可以處理 類C的實例,類D的實例,類E的實例,但不能處理類B的實例和類A的實例。

     Gen<C> gc = new Gen<C>();
        gc.set(new C());
        C c = gc.get();
        
        gc.set(new D());
        c = gc.get();
        D d = (D) gc.get();
        
        gc.set(new E());
        c = gc.get();
        d = (D) gc.get();
        E e = (E) gc.get();
        
        gc.set(new A()); //編譯錯誤
         gc.set(new B()); //編譯錯誤

繼承中的限制

           雖然,E繼承 D, D繼承C, C繼承B, B繼承A,但是Gen<A>,Gen<B>,Gen<C> ,Gen<D>,     Gen<E> 之間沒有任何繼承繼承關系。因為泛類型的本質就是增強強制轉換的安全性,將本來由程序員進行的強制轉換工作交給編譯器來完成。假設由於B繼承了A使得Gen<B>繼承Gen<A>,這就可能導致運行時的錯誤。

       Gen<B> gb = new Gen<B>();
       Gen<A> ga = gb; //編譯錯誤
        ga.set(new A());
       B b = gb.get();

         一般來說在Gen<B>放入一個B的實例,但是取出一個A的實例一般沒有什么問題,但是如果允許Gen<A> 和 Gen<B> 存在繼承關系,按照上如代碼,我們就可以讓Gen<A>的實例ga和Gen<B>的實例gb指向同一Gen<B>實例,用ga存入一個A類的實例,利用gb的get方法取出一個B類的實例,這就顯然會引起運行時的錯誤。

            泛型中的繼承的繼承情況有很多種,可以繼承一個具體的泛型類(像上述的“覆蓋”章節中    class SubGen extends Gen<A>),還一個繼承一個純粹的泛型類

class SonGen<U, T> extends Gen<T>{
    private U u;//只是定義一個變量而已
     public  void set(T argt){// 覆蓋父類中的方法
        System.out.println("I am SonGen");
    }
}

         SonGen<U, T> 繼承了 Gen<T> ,並添加了一個類型類型參數。當然還可以繼承具有泛型方法的類,這里就不舉例了。

7.通配符及類型參數的限定

             泛型中對繼承的限制是為了解決類型轉換的安全性問題,但是卻違背了java中多態的原則。為了同時解決泛型中的安全性和多態原則,java引用通配符。我們繼續使用上面的Gen<T>和類A、B、C、D、E作為例子。通配符“ ?”表示任意類型,它的使用有三種形式。

通配符與上限限定符

              Gen<? extends B >表示Gen的類型參數是類B或者任何類B的子類。Gen<? extends B >中的兩個方法(set 和 get)實際上被通配符和限定符后變成了如下的形式

              set( ? extends B )

              ? extends B get()

              我們現在來看一下它的使用

       Gen<? extends B> gec = new Gen<D>(); //編譯成功
        gec.set(new C()); //編譯錯誤
        gec.set(new D()); //編譯錯誤
        gec.set(new E()); //編譯錯誤
        B b = gec.get();  //編譯成功

         現在對上述代碼進行解釋。第一行能編譯成功,是因為滿足 D 是 B 的子類。

         最后一行沒有錯誤,因為編譯器只知道gec是對Gen<B>的子類對象的引用,而具體是哪個子類它並不清楚,但是B子類的對象一定可以轉換成B類型的對象。

          而所有調用的set方法的語句都會出現編譯錯誤,因為編譯器只知道gec是對Gen<B>的子類對象的引用,而具體是哪個子類它並不清楚(上述代碼中指向了Gen<D>的對象),所以編譯器gec把它所指的對象僅當做當做Gen<B>來使用,所以編譯器拒絕一切 ? extends B 作為參數,這樣做是保障泛型的安全性。比如有個類F,它也繼承了B類,而F類和D類不存在繼承關系。

clip_image002[7]

       Gen<D> gd = new Gen<D>();
      Gen<? extends B> gec = gd;
      gec.set(new F());//如果編譯成功
       D d = gd.get();

         若gec.set(new F())編譯通過,那么D d = gd.get()必然出現運行錯誤,因為返回的實際上是個F類型的對象,卻被轉換成了D類型。

通配符與下限限定符

              Gen<? super D>表示Gen的類型參數是類D或者任何類D的父類。Gen<? super D >中的兩個方法(set 和 get)實際上被通配符和限定符后變成了如下的形式

              set( ? super D )

              ? super D get()

             我們現在來看一下它的使用

       Gen<? super D> supd = new Gen<B>();
        supd.set(new A()); //編譯錯誤
        A a = supd.get();  //編譯錯誤
        
        supd.set(new C()); //編譯錯誤
        C c = supd.get();  //編譯錯誤
        
        supd.set(new D());
       D d = supd.get();  //編譯錯誤
        d = (D)supd.get();
       supd.set(new E());
       Object o = supd.get();

         在對上述代碼進行解釋。第一行能編譯成功,是因為滿足 B 是 D 的父類。

         如果不加強制轉換,所有調用的get方法的語句都會出現編譯錯誤,原因其實和上面類似,編譯器只知道supD是對Gen<D>的父類對象的引用,而具體是哪個父類它並不清楚,由於get的返回值類型是? super D編譯器就沒有辦法確定應該轉換成D類具體的哪種類型。

         supd.set(new D())和supd.set(new E())能夠編譯成功的原因是,雖然編譯器只知道supD是對Gen<D>的父類對象的引用,而具體是哪個父類它並不清楚,但是D類的實例和E類的實例一定能夠當做D的父類型的實例來對待。

         A是D的父類,但是以A類的對象作為參數的get 和 set 方法都會編譯失敗,這似乎和限定符表示的意思不相符。但是考慮以下的代碼:

         Gen<B> gb = new Gen<B>();
        Gen<? super D> supC = gb;
        supC.set(new A());
        B b = gb.get();

         如果upC.set(new A())能夠編譯成功,那么B b = gb.get()一定會出現異常,因為出現了向上轉型(由父類A轉向了子類B)。

無限定通配符

          Gen<?> 等價於Gen<? extends Object> 表示Gen的類型參數是Object或者Object的子類

通配符小節

       (1) extends 可用於的返回類型限定,不能用於參數類型限定。

       (2) super 可用於參數類型限定,不能用於返回類型限定。

       (3) 通配符一般用於創建泛型類的引用

         假設X表示一個具體的類,注意區別 ? extends X 和Gen<? extends X> 。Gen<? super X>和   Gen<? extends X>兩者可以作為參數類型也可以作為返回值類型,可參見 ArrayList代碼。

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");
    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

8. 泛型的局限性

        雖然java沒有真正的泛型,但是編譯器在編譯階段會做一些語法上的限制

      (1)判斷某個對象是否是泛型類的實例的正確方法

        Gen<B> gb = new Gen<B>();
        if(gb instanceof Gen<?>){
            System.out.println("only correct way");
        }

       (2) class實際上是個泛型類 原型為class<T>。A.class 是 Class<A>的唯一一個實例,String.class是Class<Stiring>的唯一一個實例

       (3)不能實例化泛型變量,即不能使用
                    new T()
                    T.class
         如果要實例化一個泛型變量,可以在泛型類中添加一個靜態方法,並傳入class<T>類型的參數

public static <T> T makeTobj(Class<T> cl){
     try {
            return cl.newInstance();
     } catch (InstantiationException | IllegalAccessException e) {
     }
     return null;
}

      (4) 沒有泛型數組,如果需要在泛型類中創建一個泛型數組,可以用Object類型的數組代替。我們可以參照ArrayList的源代碼。

public class ArrayList<E> extends AbstractList<E> 
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    //省略無關代碼
    transient Object[] elementData; 
   // non-private to simplify nested class access

    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
}

        並可以直接返回數組中的某個對象,因為編譯器會在代用該代碼時自動添加強制類型轉換。

        如果需要返回泛型的數組,我們可以new Object[] 或者利用Array.newInstance類中的方法創建特定類型的數組,然后進行強制類型轉換(T[])。參見ArrayList代碼。

public static <T,U> T[] copyOf(U[] original, int newLength,
                                  Class<? extends T[]> newType){
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), 
                                  newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

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());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

      雖然不能創建泛型數組,但是可以創建泛型數組的引用 Gen<T>[] genArr;

    (5) 沒有泛類型的靜態數據成員


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM