Java泛型中存在类型擦除机制,也就是说泛型在编译期间进行类型检验上做到有效安全,但是在运行当中,会将该泛型类型用限定类型(无限定的类型变量则用Object代替)代替,这种类型擦除带来了两个复杂的问题。
看下面代码示例:
public class A<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
//编译时会进行类型擦除
public class A {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
可以看出,泛型类型T在编译后变成了Object,虚拟机中是没有泛型的。
接着看下面这段代码:
public class B extends A<String> {
@Override
public void setValue(String value) {
}
}
我们的意图是:新写了一个 B 类,继承自 A 类,并重写A 类的 setValue 方法。
但问题出来了,我们发现 B 类中的 setValue 方法参数与 A 类中的 setValue 方法参数不一样。按照 Java 重写方法的规则,B 类中的 setValue 方法实际上并没有重写父类中的方法,而是重载。也就是说我们的意图是重写,而实际上却是重载,类型擦除与多态发生了冲突。
Java如何解决这个问题? 实际上,Java编译器会在 B 类中自动生成一个桥方法(bridge method)。
来看下B类的字节码:
// class version 51.0 (51)
// access flags 0x21
// signature Lcom/example/applaunch/A<Ljava/lang/String;>;
// declaration: com/example/applaunch/B extends com.example.applaunch.A<java.lang.String>
public class com/example/applaunch/B extends com/example/applaunch/A {
// compiled from: B.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL com/example/applaunch/A.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/example/applaunch/B; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public setValue(Ljava/lang/String;)V
L0
LINENUMBER 6 L0
RETURN
L1
LOCALVARIABLE this Lcom/example/applaunch/B; L0 L1 0
LOCALVARIABLE value Ljava/lang/String; L0 L1 1
MAXSTACK = 0
MAXLOCALS = 2
// access flags 0x1041
public synthetic bridge setValue(Ljava/lang/Object;)V
L0
LINENUMBER 3 L0
ALOAD 0
ALOAD 1
CHECKCAST java/lang/String
INVOKEVIRTUAL com/example/applaunch/B.setValue (Ljava/lang/String;)V
RETURN
L1
LOCALVARIABLE this Lcom/example/applaunch/B; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 2
}
可以看到子类B中有两个setValue方法:
public void setValue(String value)
{
...
}
public void setValue(Object value){
setValue((String)value);
}
一个参数为 String 类型,一个参数为 Object 类型,参数为 Object 类型的就是 Java 编译器帮我们生成的桥方法,桥方法内部其实就是调用了子类新定义的 setValue 方法,这样就避免了子类重写了父类的方法后还能调用到父类的方法。
同样的,如果给B类添加一个getValue方法,编译器也会自动生成一个桥方法:
public class B extends A {
public String getValue(){...}
//编译器自动生成的桥方法
public Object getValue(){
return getValue();
}
}
在java代码中,仅返回类型不同的两个方法是不合法的。但是,在虚拟机中,用参数类型和返回类型确定一个方法。因此, 编译器可能产生两个仅返回类型不同的方法的字节码,虚拟机能够正确地处理这一情况。