Java 泛型总结

java

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

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方法,publicvoid set(javaleanning.A arg0)是我们自己编写了,而另一个publicvolatilevoid set(java.lang.Object arg0) 是编译器自动帮我们添加的(注意代码的注释bridge method generated by the compiler)。添加的这个方法正好和父类中擦除掉泛型的set方法同名且同参数publicvolatilevoid 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类不存在继承关系。

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

以上是 Java 泛型总结 的全部内容, 来源链接: utcz.com/z/393481.html

回到顶部