声明:转载自https://blog.csdn.net/hao_yan_bing/article/details/89447792
前几天在看 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是能分辨的