Java编译器将Java代码编译成过程中,会由编译器引入一些类、方法、属性,这些由编译器引入一些类、方法、属性会被标上synthetic字段。本文主要介绍几种编译器会生成synthetic方法的情况
1、泛型
子类在具象化泛型接口、泛型父类的泛型时,由于方法签名不一致,运行时动态分派将会查找不到重写的方法,为解决此问题,编译器会生成一个synthetic bridge方法
public class Base<T> {
public void f(T t) {
System.out.println("Base");
}
}
public class Sub extends Base<String> {
@Override
public void f(String s) {
System.out.println("Sub");
}
}
public class Main {
public static void main(String[] args) {
Base<String> base = new Sub();
base.f("123");
}
}
// 输出Sub
Base类编译后,由于泛型擦除,f方法签名将变为f(Ljava/lang/Object;)V,即入参为Object类型。Sub类不存在泛型,所以编译后,f方法的签名将变为f(Ljava/lang/String;)V。由于这两个方法签名不同,Sub#f方法并未真正意义上重写Base#f方法。这就存在一个问题,main方法里调用Base#f时,由于泛型擦除和静态分派,调用方法的签名是f(Ljava/lang/Object;)V;运行时动态分派根据实际类型去查找f(Ljava/lang/Object;)V方法,而Sub里并不存在该方法,顺着类继承层次会从父类中查找到该方法,从而调用父类方法,子类重写的方法失效。
为了解决这个问题,编译器在子类Sub中生成一个签名为f(Ljava/lang/Object;)V的方法,这个方法调用重写的f(Ljava/lang/String;)V方法。
用jclasslib插件查看Base类f方法编译出来的字节码
用jclasslib插件查看Sub类f方法编译出来的字节码,存在两个f方法。重写的f方法
编译器生成的f方法
2、子类重写方法改变方法签名
如上是泛型导致的方法签名不一致,编译器生成一个synthetic bridge方法。子类在重写父类方法时,可以修改方法的返回值,也会导致啊方法签名不一致,也需要编译器生成一个synthetic bridge方法。
public class Base {
public Number f() {
return 1.11;
}
}
public class Sub extends Base {
@Override
public Integer f() {
return 1;
}
}
public class Main {
public static void main(String[] args) {
Base base = new Sub();
System.out.println(base.f());
}
}
// 输出1
跟上面类似
3、可见性问题
由于反射设计问题,反射的方法访问权限控制与JVM访问权限控制存在差异,导致能直接调用的方法通过反射调用却存在问题,为解决此种不一致,编译器引入synthetic方法。
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6342411
// package org.example.refl.a
class Base {
public void f() {
System.out.println("Base");
}
}
public class Sub extends Base {
}
// package org.example.refl
public class Main {
public static void main(String[] args) {
Sub sub = new Sub();
sub.f();
}
}
// package org.example.refl
public class Main {
public static void main(String[] args) throws Exception {
Method f = Sub.class.getMethod("f");
f.invoke(new Sub());
}
}
org.example.refl.a包下面定义包可见的Base类和public可见的Sub类。Base类的f方法对Main类不可见,但对Sub可见;Sub类对Main类可见。 通过方法调用和反射两种方式调用Sub#f方法。
直接方法调用时,main方法编译成如下
invokevirtual的方法访问权限控制涉及三个阶段:验证、解析、执行阶段,本节问题只涉及解析阶段中的方法解析。解析就是将符号引用转换成常量池中的具体值引用。 方法解析将reference method解析为resolved method。reference method即invokevirtual指令操作数指定的方法,resolved method即常量池中的某个方法。方法解析首先按照类继承层次根据符号引用查找方法,然后校验权限。由于符号引用指定的方法可能不存在(继承父类获得的方法),导致reference method解析为resolved method不是同一个方法,但是校验权限仍按照reference method校验,所以Main类中可以调用org/example/refl/a/Sub.f。
通过反射调用方法时,实际上需要reflect包去实现JVM同样的权限校验逻辑,此时存在了不一致。由于Sub类没有f方法,Sub.class.getMethod(“f”)实际上会获得到Base#f方法,此时校验权限则不通过,因为Main中不能访问Base#f方法。为解决此问题,编译器在Sub类中加入了synthetic方法,在这个方法中调用Base#f方法。此时Sub.class.getMethod(“f”)拿到的方法是这个synthetic方法,不存在访问权限问题。