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) 没有泛类型的静态数据成员

posted @ 2015-12-12 13:09  nullzx  阅读(2029)  评论(1编辑  收藏  举报