虽然编写代码时可以使用泛型,但是泛型实际上是一种语法糖,虚拟机并不认识泛型,编译器在编译时会进行类型擦除。
- 无限定的类型变量,替换为
Object
类型 - 使用了限定符的类型变量,替换为对应的限定类型。
<T extends Comparable>
替换为Comparable
<T extends Serializable & Comparable>
,多类型限定,类型擦除时,替换为第一个类型Serializable
。编译器将在有必要的时候向Comparable
插入强制类型转换。为了提高效率,应该将标签接口(没有方法的接口)放在后面。 - 类型擦除后,为了保证类型安全,必要时将插入强制类型转换。
例如:
ArrayList<String> list = new ArrayList<>();
list.add("no");
String str = list.get(0);
// 类型擦除后,第三行其实为:String str = (String) list.get(0);
假若没有泛型,我们将要自行进行强制类型转换,假如我们不小心加入了一个File
对象,这就会抛出异常。
上面是泛型工作的基本机制,不过,在翻译泛型方法时,会出现比较复杂的问题。
例如:
public class Pet<T> {
private T arg;
public void setArg(T arg) {
this.arg = arg;
}
public T getArg() {
return arg;
}
}
class Cat extends Pet<String> {
@Override
public void setArg(String arg) {
...
}
@Override
public String getArg() {
...
}
}
假如我们这样调用:
Pet<String> pet = new Cat();
pet.setArg("cocoa");
String str = pet.getArg();
这里希望对setArg
的调用具有多态性,调用最合适的方法。引用的是Cat
类型,那么应该调用Cat.setArg
。问题在于多态和类型擦除产生了冲突,要解决这个问题,就需要编译器在Cat
类中生成一个桥方法。
public void setArg(Object arg) {
setArg((String) arg);
}
补充: 其实也可以这样理解,类型擦除后,Pet.setArg(Object)
与子类试图重载的方法Cat.setArg(String)
拥有了不一样的签名,在那一行调用pet.setArg("cocoa")
的代码中,只会调用父类的Pet.setArg(Object)
而不是调用子类Cat.setArg(String)
这个方法。为了解决这个问题,编译器会在子类Cat
加入如上所示的桥方法,强制类型转换后去调用Cat.setArg(String)
。
因为类型擦除的原因,Cat
类重写Pet.getArg()
时,编译时也会多出现一个桥方法,这才是实际上重写了父类Pet.getArg()
的方法。
public Object getArg() {
...
}
此时在类中出现了两个签名相同的方法!
String getArg()
Object getArg()
虽然不能这样编写java代码(无法通过编译)。但是,在虚拟机中,用参数类型和返回类型确定一个方法。因此, 编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。