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
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类不存在继承关系。
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) 没有泛类型的静态数据成员