类型擦除
事实上,虚拟机是没有泛型类型对象的———所有对象都属于普通类。它所看到的只有原始类型。
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型 ( raw type)。原始类型的名字就是删去类型参数后的泛型类型名。当编译一个泛型类的时候,编译器会擦除那些泛型类型名,并将它转换为第一个限定类型名。这样就得到了原始类型。
比如说:
class Demo<T extends Comparable & Iterable>
{
private T field;
//...
}
擦除后,T
自动被转换为第一个限定类型Comparable
,原始类型就是这样的:
Class Demo
{
private Comparable field;
//...
}
附注:由以上例子可以知道:为了提高效率,应该将标签(tagging) 接口(即没有方法的接口)放在边界列表的末尾。
而对于没有限定的泛型类:
class Demo<T>
{
private T field;
//...
}
将自动转换为Object
:
class Demo
{
private Object field;
//...
}
也就是说,看上去像是泛型的类,实际上是一个普通的类。就好像泛型引人Java语言之前已经实现的那样。
而在使用时,在程序中可以包含不同类型的Demo
, 例如, Demo<String>
或 Demo<LocalDate>
。而擦除类 型后就变成原始的Demo
类型了。
翻译泛型表达式
根据以上描述可以得知,在编译过后,虚拟机看到的只有原始类型。所以当使用泛型表达式 时,就需要进行翻译。
具体而言,当程序调用泛型方法时,如果擦除返回类型, 编译器插入强制类型转换。比如对于:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
在我们编程时,getFirst
返回一个类型T
的对象;擦除后返回类型成了Object
。编译器自动插人Employee
的强制类型转换。 也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法 Pair.getFirst
的调用
- 将返回的Object
类型强制转换为Employee
类型
继承泛型类的的问题
擦除会带来一个问题,假设有这样一个类:
class Value<T>
{
private T value;
public void setValue(T val)
{
value = val;
}
public T getValue()
{
return value;
}
}
现在假设有一个新的类继承了这个泛型类:
class Int extends Value<Integer>
{
@Override
public void setValue(Integer val)
{
super.setValue(val);
}
public Integer getValue()
{
return super.getValue();
}
}
多态性与桥方法
说明
现在,使用一个Value<Integer>
的引用调用Int
对象的方法,希望可以实现多态性。
Int i = new Int();
i.setValue(3);
System.out.println(i.getValue());
Value<Integer> i2 = i;
i2.setValue(4);//是否可以实现多态性呢?
System.out.println(i2.getValue());
乍一想这似乎不是个问题,但是仔细分析就会发现有问题。以setValue
方法为例,由于Value<Integer>
在编译后会进行擦除,所以setValue
实际上并不是:
public void setValue(Integer val)
{
value = val;
}
而是而是一个参数类型为Object
的方法。
public void setValue(Object val)
{
value = val;
}
也就是说在擦除后,超类的setValue
方法和子类的setValue
的参数类型不同,前者为Object
而后者为Integer
。
但是不应该这样啊!这就意味着我们以为我们在子类Int
中覆盖了超类的方法,然而实际上我们只是重新定义了一个同名的重载方法。!
class Int extends Value<Integer>
{
//这是从超类继承的
public void setValue(Object val){}
//这是我们新定义的,它并没有实现对超类方法的Override
public void setValue(Integer val){...}
}
这就意味着,我们很可能无法实现多态性。
好在,为了解决这个问题,编译器会自动生成一个桥方法。
public void setValue(Object val)
{
setValue((Integer)val);
}
通过这个编译器自动生成的桥方法,就可以实现多态性。下面我们来跟踪一下语句的执行过程:
Value<Integer> i2 = i;
i2.setValue(4);
i2
是一个Value<Integer>
类型,这个类中唯一一个和函数名匹配的方法只有setValue(Object)
。虚拟机用引用的对象调用这个方法。- 检测到该对象实际上是一个
Int
类,于是调用合成的桥方法setValue(Object)
,多态性实现。
桥方法的检验
为了证实桥方法确实存在,我们可以利用反射来加以检测。
首先,出于对照试验的考虑我们来额外定义一组普通的继承体系:
class Value2
{
private int val = 0;
public void setValue(int val)
{
this.val = val;
}
public int getValue()
{
return val;
}
}
class Int2 extends Value2
{
@Override
public int getValue()
{
return super.getValue();
}
@Override
public void setValue(int val)
{
super.setValue(val);
}
}
定义一个Int2 i2 = new Int2();
调用预先写好的displayDeclaredMethod(ix);
。输出如下:
Class name : Int2
Declared method:
int getValue()
void setValue(int arg0, )
然而对于一个Int i
的对象,调用displayDeclaredMethod(ix);
,输出:
Class name : Int
Declared method:
Object getValue()
Integer getValue()
void setValue(Object arg0, )
void setValue(Integer arg0, )
显然,多出了两个编译器自动生成的桥方法。
方法签名问题
如果你仔细看一看之前输出的Int
的方法,你会发现一个奇怪的地方:
Object getValue()
Integer getValue()
发现了没有?在一个类中竟然出现了两个方法签名相同的方法!除了返回值,其他的都相同!
事实上关于区分方法有一个很冷门的知识点:
- 方法签名 确实只有方法名+参数列表 。这毫无疑问!
- 我们绝对不能编写出方法签名一样的多个方法 。如果这样写程序,编译器是不会放过的。这也毫无疑问!
- 事实上,JVM用参数类型和返回类型确定一个方法。因此, 编译器可能产 生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。
简而言之,主要的锅在编译器上。编译器限制了我们的行为,但却给自己“开后门”。
引用:
http://blog.youkuaiyun.com/pacosonswjtu/article/details/50374131
《Java 核心技术》