前几天在看 Java 泛型的时候,发现了一个有趣的现象。就是在某些情况下,编译器在编译我们的类文件的时候会帮我们自动生成某些方法,称作桥方法。
我们知道 Java 中的泛型在编译为 class 的时候会把泛型擦除,也就是说你写的 <T> 到最后 class 文件中其实都是 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;
}
}
可以看出,Java 中的泛型在编译后都变成了 Object,也可以说 Java 中的泛型其实是编译器为我们做了优化,虚拟机中是没有泛型的。
那我们接着看下面这段代码:
public class B extends A<String> {
@Override
public void setValue(String value) {
System.out.println("---B.setValue()---");
}
}
我们写了一个 B 类,继承自 A 类,并重写了 setValue 方法。
我们来思考一个问题,按我们上面所说的 Java 泛型的擦除机制,实际 A 类中 setValue 方法应该是这样的:
// A 类中的 setValue 方法
public void setValue(Object value){
this.value = value;
}
这个时候问题出来了,我们发现 B 类中的 setValue 方法参数与 A 类中的 setValue 方法参数不一样。按照 Java 重写方法的规则,B 类中的 setValue 方法实际上并没有重写父类中的方法,而是重载。
所以实际上 B 类中应该是有两个 setValue 方法,一个自己的,一个继承来的:
// 自己的
public void setValue(String value){...}
// 从父类继承的
public void setValue(Object value){...}
所以在某些场景,比如反射调用 B 类中的方法的时候,就有可能会调用到从父类继承的那个 setValue 方法。
这个时候就会出现与我们意愿不一致的结果了,违反了我们重写方法的意愿了。
当然,这种情况是不会出现的,因为 Java 编译器帮我们处理了这种情况。我们来查看 B.class 字节码文件:
// class version 52.0 (52)
// access flags 0x21
// signature LA<Ljava/lang/String;>;
// declaration: B extends A<java.lang.String>
public class B extends A {
// compiled from: B.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 4 L0
ALOAD 0
INVOKESPECIAL A.<init> ()V
RETURN
L1
LOCALVARIABLE this LB; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public setValue(Ljava/lang/String;)V
L0
LINENUMBER 8 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "---B.setValue()---"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 9 L1
RETURN
L2
LOCALVARIABLE this LB; L0 L2 0
LOCALVARIABLE value Ljava/lang/String; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1041
public synthetic bridge setValue(Ljava/lang/Object;)V
L0
LINENUMBER 4 L0
ALOAD 0
ALOAD 1
CHECKCAST java/lang/String
INVOKEVIRTUAL B.setValue (Ljava/lang/String;)V
RETURN
L1
LOCALVARIABLE this LB; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 2
}
我们看到 B 类中有两个 setValue 方法,一个参数为 String 类型,一个参数为 Object 类型,参数为 Object 类型的就是 Java 编译器帮我们生成的桥方法,实际代码如下:
public void setValue(String value){...}
public void setValue(Object value){
setValue((String)value);
}
桥方法内部其实就是调用了我们自己的 setValue 方法,这样就避免了在重写的时候我们还能调用到父类的方法。
问题还没有完,我们接着看:
public class B extends A<String> {
@Override
public String getValue() {
return super.getValue();
}
}
B 类重写了 A 类中的 getValue 方法。按照泛型的擦除,父类中的 getValue 方法返回值其实是 Object。
所以其实编译器也帮我们生成了桥方法,这里就不贴字节码文件了,大家可以自己查看。编译后的 B 类其实是这样:
public class B extends A {
// 自己定义的方法
public String getValue(){...}
// 编译器生成的桥方法
public Object getValue(){
return getValue();
}
}
这个时候我们发现 B 类有点颠覆我们的常识了,难道一个类中允许出现方法签名相同的多个方法?
- 方法签名确实是方法名+参数列表
- 我们也不能在同一个类中写两个方法签名相同的方法
- JVM 会用方法名、参数类型和返回类型来确定一个方法,所以针对方法签名相同的两个方法,返回值类型不相同的时候,JVM是能分辨的
当然这种情况不只是在使用泛型的时候会出现,当在重写方法时,指定了一个更加严格的返回值类型,虚拟机会帮我们生成桥方法。该例子中的 A.getValue() 和 B.getValue() 称为具有协变的返回类型。
网友评论