引言
上一篇文章我们简单地了解了泛型的定义和使用,并且知道,在使用了泛型之后, 代码在编译期间就被确保是类型安全的,不会出现类转换异常。泛型通过什么手段来达到这个目的呢?我们这篇文章就来探讨这个问题。
不过这里不会首先就去讲解擦除的原理,而是通过对比反编译前后的泛型类文件来开始认识泛型擦除。
反编译后的结果
泛型类
我们还是以上一篇文章中的GenericHolder1为例:
public class GenericHolder1<T> {
private T t;
public GenericHolder1(T t) {
this.t = t;
}
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
GenericHolder1<String> genericHolder1 = new GenericHolder1<>("we");
String t = genericHolder1.getT();
}
}
我们使用jad来反编译GenericHoler1生成的class文件,我们会看到下面的结果:
public class GenericHolder1
{
public GenericHolder1(Object t)
{
this.t = t;
}
public Object getT()
{
return t;
}
public void setT(Object t)
{
this.t = t;
}
public static void main(String args[])
{
GenericHolder1 genericHolder1 = new GenericHolder1("we");
String t = (String)genericHolder1.getT();
}
private Object t;
}
我们可以发现,反编译后的文件没有一点泛型的影子,GenericHolder1这个类的声明没有其他的东西了,只是一个Public Class GenericHolder1,这就是一个简单的类声明。在每个出现T的位置,都用Object进行了替换,main方法里面对getT方法的调用,编译器自动为我们做了强制类型转换(因为在创建GenericHolder1的实例时,我们已经指定了泛型的精确类型是String)。也就是说,编译器用Object替换了T,但是当真正指定了T的精确类型之后,还是需要做强制类型转换的,只是编译器为我们自动做了,这也就是为什么不需要我们再去进行类型转换而直接可以得到String的原因。
这就是泛型的擦除,在编译期间,泛型信息被完全擦除,真正运行的是一个没有泛型信息的类。
再来看另外一个例子:
public class GenericHolder2<T extends SimpleObject> {
private T t;
public GenericHolder2(T t) {
this.t = t;
}
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
GenericHolder2<SimpleObject> genericHolder2 = new GenericHolder2<>(new SimpleObject());
SimpleObject s = genericHolder2.getT();
}
}
与GenericHolder1不同的是,我们的泛型参数T这次继承了SimpleObject,意思就是T可以是任何SimpleObject的子类。反编译后的文件如下:
public class GenericHolder2
{
public GenericHolder2(SimpleObject t)
{
this.t = t;
}
public SimpleObject getT()
{
return t;
}
public void setT(SimpleObject t)
{
this.t = t;
}
public static void main(String args[])
{
GenericHolder2 genericHolder2 = new GenericHolder2(new SimpleObject());
SimpleObject s = genericHolder2.getT();
}
private SimpleObject t;
}
替换掉T的不再是Object,而是SimpleObject,与Object不同的是,编译器不再需要为我们做强制类型转换了,因为每个用到T的位置,不管是getT的返回值,还是setT的参数,都是精确的SimpleObject的类型。
通过这两个例子,我们应该了解了泛型擦除的原理,就是在编译期间将泛型参数替换为真实的边界,然后在每个用到泛型的位置通过加上强制类型转换(Object)或者不加(例如SimpleObject)来实现类型的安全转换。泛型类能够做到这点很重要的原因是,在初始化泛型类或者泛型接口时,我们需要指定精确的泛型类型,编译器通过我们指定的精确类型,来完成擦除。
泛型方法
我们再来看上一篇文章中泛型方法的例子:
public class GenericMethods {
public <T> T getObject(T t){
return t;
}
public static void main(String[] args) {
GenericMethods genericMethods = new GenericMethods();
Integer object = genericMethods.getObject(1);
String qwe = genericMethods.getObject("qwe");
SimpleObject object1 = genericMethods.getObject(new SimpleObject());
}
}
泛型方法与泛型类、泛型接口不同的是,没有初始化的步骤,只是直接的参数传递,那么泛型方法是怎样保证类型安全的呢?还是看反编译后的代码:
public class GenericMethods
{
public GenericMethods()
{
}
public Object getObject(Object t)
{
return t;
}
public static void main(String args[])
{
GenericMethods genericMethods = new GenericMethods();
Integer object = (Integer)genericMethods.getObject(Integer.valueOf(1));
String qwe = (String)genericMethods.getObject("qwe");
SimpleObject object1 = (SimpleObject)genericMethods.getObject(new SimpleObject());
}
}
T还是被替换成了Object,然后编译器同样为方法返回值加上了强制类型转换,那么它怎么知道需要转换成什么类型呢?因为我们在传递参数的时候已经说明了T的类型,例如当传入1时说明类型是Integer,编译器就知道返回值是Integer,当传入SimpleObject时编译器就知道要返回SimpleObject。
再来看下面这个方法:
public class GenericMethods {
public <T extends SimpleObject,E> E getObject1(T t,E e){
System.out.println(e.getClass().getName());
return e;
}
public static void main(String[] args) {
GenericMethods genericMethods = new GenericMethods();
Integer integer = genericMethods.getObject1(new SimpleObject(), 1);
String string = genericMethods.getObject1(new SimpleObject(), "a string");
}
}
getObject1方法有两个泛型参数,T指定必须是SimpleObject的子类,E没有指定边界,所以我们猜想,T会被替换为SimpleObject,E会被替换为Object,看反编译后的代码:
public class GenericMethods
{
public GenericMethods()
{
}
public Object getObject1(SimpleObject t, Object e)
{
System.out.println(e.getClass().getName());
return e;
}
public static void main(String args[])
{
GenericMethods genericMethods = new GenericMethods();
Integer integer = (Integer)genericMethods.getObject1(new SimpleObject(), Integer.valueOf(1));
String string = (String)genericMethods.getObject1(new SimpleObject(), "a string");
}
}
结果和我们的猜想一样。由于返回值类型被替换为Object,所以在main方法中的调用,编译器自动为我们加上了强制类型转换来保证类型安全。
通过上面的四个例子,我们知道了擦除是怎样实现的:在泛型类或者泛型接口中,编译器通过对泛型类型参数的声明来选择替换成哪种准确的类型,例如将没有边界的T替换为Object,将T extends SimpleObject替换为SImpleObject。由于我们在初始化的时候需要为泛型参数指定精确的类型,所以编译器会知道我们想要什么类型,就能够通过自动加上类型转换(当被擦除为Object的时候)或者不加(当被替换为类似SimpleObject的时候)来保证类型的安全转换。
在泛型方法中,编译器同样通过泛型参数的声明来进行擦除,虽然没有初始化过程,但是编译器能够通过我们传递的参数判断出我们想要的类型,进而保证类型安全。
由于泛型在编译期间被擦除了,所以在运行期间,每个泛型实例的类型总是相同的,而不管它们指定了什么样的精确类型。看下面的例子:
public static void main(String[] args) {
GenericHolder1<String> a = new GenericHolder1<>("we");
GenericHolder1<Integer> b = new GenericHolder1<>(1);
System.out.println(a.getClass() == b.getClass());
System.out.println(a.getClass().getName());
}
输出:
person.andy.concurrency.generic.GenericHolder1
true
这就是使用擦除来实现泛型的结果。a和b虽然在声明时分别指定了String和Integer作为类型参数的精确类型,但是GenericHolder1这个类在编译期间就被擦除为普通的类型了,所以a和b都只是被擦除后的普通类型而已。
类似的,java中的集合也会是这样的情况:
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass()==integers.getClass());
}
使用桥接方法来保护多态
当类型信息被擦除后,方法的继承就会出现问题,看下面的例子:
public class ObjectContainer<T> {
private T contained;
public ObjectContainer(T contained) {
this.contained = contained;
}
public T getContained() {
return contained;
}
public void setContained(T contained) {
this.contained = contained;
}
}
public class FruitContainer extends ObjectContainer<Fruit> {
public FruitContainer(Fruit contained) {
super(contained);
}
@Override
public void setContained(Fruit contained) {
super.setContained(contained);
}
public static void main(String[] args) {
FruitContainer fruitContainer = new FruitContainer(new Apple());
fruitContainer.setContained(new Apple());
}
}
FruitContainer继承了ObjectContainer并且指定了精确类型Fruit,所以setContained()方法的参数变成了Fruit,而ObjectContainer的setContained()方法在被擦除后应该是Object,在父类和子类中的方法参数类型并不相同,也就是FruitContainer的setContained()方法并没有override ObjectContainer的setContained()方法,当遇到这种情况,编译器会自动为我们添加一个桥方法,看FruitContainer反编译后的文件:
public class FruitContainer extends ObjectContainer
{
public FruitContainer(Fruit contained)
{
super(contained);
}
public void setContained(Fruit contained)
{
super.setContained(contained);
}
public static void main(String args[])
{
FruitContainer fruitContainer = new FruitContainer(new Apple());
fruitContainer.setContained(new Apple());
}
public volatile void setContained(Object obj)
{
setContained((Fruit)obj);
}
}
可以看到反编译后的文件比源文件多了一个setContained(Object obj)方法,这个方法直接调用了原有的setContained()这个方法,这就是桥方法,编译器通过自动添加这个方法,保护了继承关系中的多态。
边界
在前面的GenericHolder1中,我们简单的使用了T指定了泛型参数,通过编译后的代码我们知道,T被替换成了Object,在没有给定任何边界的情况下,Object会作为泛型参数的默认边界。
但是有时我们想调用特定类或者接口的方法,这时我们可以通过定义泛型的边界来实现。GenericHolder2就是一个使用了自定义边界的例子,我们通过extends方法指定泛型参数继承了SimpleObject,此时T的边界就是SimpleObject。
从编译后的文件我们也能看到。当有了SimpleObject作为边界之后,我们就可以执行它的方法了,看下面的例子:
public interface HasColor {
java.awt.Color getColor();
}
public class WithColor<T extends HasColor> {
private T item;
public WithColor(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public java.awt.Color getColor(){
return item.getColor();
}
}
泛型类WithColor的类型变量T被指定需要实现HasColor接口,所以边界就是HasColor,这样就可以在getColor里面调用item的getColor方法了。
根据之前的经验,这个类反编译之后T会被HasColor替代,所以对其调用getColor肯定是可以的:
public class WithColor
{
public WithColor(HasColor item)
{
this.item = item;
}
public HasColor getItem()
{
return item;
}
public Color getColor()
{
return item.getColor();
}
private HasColor item;
}
继续我们的例子,我们添加一个类Coord:
public class Coord {
public int x,y,z;
public int getCoords(){
return x + y + z;
};
}
然后添加泛型类,该泛型类的类型参数需要继承Coord并实现HasColor:
public class WithColorCoord<T extends Coord & HasColor> {
T item;
public WithColorCoord(T item) {
this.item = item;
}
public T getItem() {
return item;
}
java.awt.Color getColor(){
return item.getColor();
}
int getX(){
return item.x;
}
int getY(){
return item.y;
}
int getZ(){
return item.z;
}
int getCoord(){
return item.getCoords();
}
}
T类型的item能够调用Coord和HasColor两者的方法,我们看一下反编译后的代码:
public class WithColorCoord
{
public WithColorCoord(Coord item)
{
this.item = item;
}
public Coord getItem()
{
return item;
}
Color getColor()
{
return ((HasColor)item).getColor();
}
int getX()
{
return item.x;
}
int getY()
{
return item.y;
}
int getZ()
{
return item.z;
}
int getCoord()
{
return item.getCoords();
}
Coord item;
}
编译器用Coord类替换了T,然后在每个调用Coord的方法上,就能直接使用了,而在调用HasColor的方法时,进行了强制类型转换,这种转换也是没有问题的。
继续增加一个接口:
interface Weight {
int weight();
}
修改泛型类:
public class Solid<T extends Coord & HasColor & Weight> {
T item;
public Solid(T item) {
this.item = item;
}
public T getItem() {
return item;
}
java.awt.Color color(){
return item.getColor();
}
int getX(){
return item.x;
}
int getY(){
return item.y;
}
int getZ(){
return item.z;
}
int weight(){
return item.weight();
}
}
增加了weight接口之后,我们也可以在T上面调用weight的方法了,看编译后的代码:
public class Solid
{
public Solid(Coord item)
{
this.item = item;
}
public Coord getItem()
{
return item;
}
Color color()
{
return ((HasColor)item).getColor();
}
int getX()
{
return item.x;
}
int getY()
{
return item.y;
}
int getZ()
{
return item.z;
}
int weight()
{
return ((Weight)item).weight();
}
Coord item;
}
item还是被Coord替代,在使用weight和hasColor的部分,都是通过强制类型转换来做的。
也就是说,对于泛型,在没有给定边界的情况下,默认边界就是Object,在给定边界的情况下,边界是extends关键字后面的第一个类型T,注意当extends后面有多个类型例如
还有一点需要注意,在声明泛型类、泛型接口和泛型方法的类型变量时,都只能使用extends来指定边界,而不能使用super,这个要跟后面将要讲到的协变和逆变分清楚。
小结
这篇文章从泛型类和泛型方法反编译后的文件出发,分析了java泛型擦除的实现方式,我们应该知道编译器在泛型擦除的过程中起了很大的作用,它根据我们的泛型定义生成了擦除后的代码,为泛型找到了合适的边界。使用擦除来实现泛型同样导致了很多问题,例如不能使用instanceOf关键字,这些都可以通过其他方式来解决。
下一篇文章,我们会介绍泛型的另外一个问题—协变和逆变。