Java的泛型在代码中引用时,形式上很类似于普通的类,可以用来声明对象、方法的参数与返回值类型,甚至还可以作强制类型转换。因此,容易误认为泛型是一个“类名的变量”,当泛型类被参数化引用的时候,这个“变量”就被“赋值”为实际类。事实上这种认识是一个误区。
理论上说,Java中的类是由对应的.class文件的字节码来定义的,类在其编译时就会产生.class文件,与其如何被引用没有任何关系,当然更不可能与引用时的参数相关。编写泛型类时,也无从获取实际引用时的参数类,如Class clz=T.class这样的语句是无法通过编译的(考虑到引用时参数类可以是通配符,这点也就不奇怪了)。
既然泛型不是类,也不能获取引用时的参数类,那么以泛型声明的对象,是以什么类型编译的呢?答案是声明泛型时的上界类。为了证实这一点,来看一个最简单的自定义泛型类,只包含一个泛型声明的属性t及其get和set方法:
用jdk提供的javap工具可以查看.class文件的汇编代码,结果如下:
可以看到,类体中用到的泛型T,编译后都替换成了其上界类Number,而泛型特征在编译后被完全抹去了。如果反编译GenericClassTest.class文件,可以看的更清楚:
鉴于这种特性,在编写泛型类代码的时候要尤其谨慎使用泛型进行强制类型转换,如T t=(T)(对象);这样的语句在编译时会被认作是强制类型转换到泛型T的上界。由于编译器无从得知T的准确类型,这种转换无法用instanceof关键字预先进行匹配性的判断(instanceof T无法通过编译)。因此,如果使用不当,就会带来强制类型转换异常的隐患,尤其是转换后的结果被引用的时候。如下是一个刻意造成这种错误的例子:
以上的泛型类在get方法中给泛型属性t强制赋值一个字符串。由于T的编译上界类是Object,这个语句可以通过编译(但是会触发“未经检验的类型转换”警告)。引用时,声明了一个以Integer为参数的对象gct,在gct的作用域(main函数)中,其属性t被编译器认为是Integer类型,因此将其赋值给整数对象i,编译也不会有问题。但是运行时就会出现类型转换异常
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at test.GenericClassTest.main(GenericClassTest.java:13)
既然泛型类编译后只与泛型上界有关,为何在使用泛型类的对象时,又表现出强类型的特征呢(例如无法向一个ArrayList<String>声明的对象中add一个整数)?事实上这是Java编译器在引用泛型类的时候做了一次转义。为此写一个测试方法:
方法中声明了一个泛型GenericClassTest的对象,引用参数为Integer,并且执行了一次set和get方法。反编译.class文件,相应方法代码如下:
可以看到,代码中同样没有泛型特征,说明编译器在正式编译前,做了一步“预处理”,将涉及泛型引用的对象强制转换为泛型参数所指定的类型,也就是Integer类(注:如果声明泛型对象是参数带有通配符,则原先的get和set方法只有一个可以被引用:指定上界时只能使用get方法,指定下界时只能使用set方法,否则编译不通过。原因涉及通配符捕获概念,参见http://www.ibm.com/developerworks/cn/java/j-jtp04298.html)
接下编译器才进行“真正的编译”,生成.class文件。
这种预处理有什么用呢?好处就是编译器写好了一套规范来实现强制类型转换,使得从使用外层看带参的泛型可以当作强类型来用。如果有不正确的类型操作,在编译阶段就会及时报错。
作者:编程趋势
出处:http://www.cnblogs.com/codetrend/
作者微博:http://weibo.com/liuxue9527
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。