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