桥接方法isBridge() 和 合成方法isSynthetic()的功能探究
今天在看spring的时候看到这样一段代码
public abstract class ReflectionUtils {
public static final MethodFilter USER_DECLARED_METHODS =
(method -> !method.isBridge() && !method.isSynthetic());
}
其中 isBridge()
和 isSynthetic()
分别用来判断方法是否为桥接方法
和合成方法
,那么接下来我们就看下他俩到底有什么作用?
1.桥接方法
桥接方法是在jdk5引入泛型后,为了使泛型方法生成的字节码和之前的版本相兼容,而由编译器自动生成的方法。
编译器是在什么时候会生成桥接方法呢?这个在官方的JLS中也有说明,可以具体看下。
当子类在继承(或实现)一个带有泛型的父类(或接口)时,在子类中明确指定了泛型,此时编译器在编译时就会自动生成桥接方法。
1.1 从字节码看桥接方法
我们通过一段代码来看下:
//接口
public interface Action<T> {
T play(T action);
}
//实现类
public class Children implements Action<String> {
@Override
public String play(String action) {
return "play basketball.....";
}
}
我们将实现类Children编译看下字节码:
Compiled from "Children.java"
public class com.qiuguan.juc.bridge.Children extends java.lang.Object implements com.qiuguan.juc.bridge.Action<java.lang.String>
{
public com.qiuguan.juc.bridge.Children();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/qiuguan/juc/bridge/Children;
public java.lang.String play(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: ldc #2 // String play basketball.....
2: areturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this Lcom/qiuguan/juc/bridge/Children;
0 3 1 action Ljava/lang/String;
//这个方法我们并没有定义,这个就是编译器自动生成的桥接方法
public java.lang.Object play(java.lang.Object);
descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #3 // class java/lang/String
5: invokevirtual #4 // Method play:(Ljava/lang/String;)Ljava/lang/String;
8: areturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/qiuguan/juc/bridge/Children;
}
Signature: #21 // Ljava/lang/Object;Lcom/qiuguan/juc/bridge/Action<Ljava/lang/String;>;
SourceFile: "Children.java"
从字节码中可以看到,一共有3个方法,第一个是无参构造器,第二个是我们实现了接口的方法,而第三个就是编译器生成的桥接方法,单独看下这个桥接方法:
public java.lang.Object play(java.lang.Object);
descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
//ACC_BRIDGE: 桥接方法的标识
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #3 // class java/lang/String
5: invokevirtual #4 // Method play:(Ljava/lang/String;)Ljava/lang/String;
8: areturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/qiuguan/juc/bridge/Children;
可以看到它含有一个 ACC_BRIDGE 的标识,表明他是一个桥接方法,而且他的返回值类型和参数类型都是java.lang.Object,从字节码中的第9行可以看到,它会将Object转成String类型,然后再调用Children
类中声明的方法。转换一下就是
public Object play(Object object) {
return this.play((String)object);
}
所以说,桥接方法实际上调用了具体泛型的方法,看下下面的这段代码:
public class Test {
public static void main(String[] args) {
//接口不指定泛型
Action children = new Children();
System.out.println(children.play("basketball"));
System.out.println(children.play(new Object()));
}
}
父接口不指定泛型,那么在方法调用时就可以传任何参数,因为Action
接口的方法参数实际上是Object
类型,此时我传String
或者Object
都可以,都不会报错。在运行时参数类型不是Children声明的类型时,才会抛出类型转换异常,上面的代码输出就是这样:
play basketball.....
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String
at com.qiuguan.juc.bridge.Children.play(Children.java:7)
at com.qiuguan.juc.bridge.Test.main(Test.java:21)
如果我们再声明 Action接口时指定泛型,比如:
Action<String> children = new Children();
当然这里只能是String类型,因为Children类的泛型类型就是String,如果指定其他类型,那么在编译时就会报错,这样就把类型检查从运行时提前到了编译时,这就是泛型的好处。
1.2 从反射看桥接方法
还是使用上面的例子,我们通过反射来看下:
public class Test {
public static void main(String[] args) {
Method[] declaredMethods = Children.class.getDeclaredMethods();
for (Method m : declaredMethods) {
System.out.printf("methodName = %s , paramType = %s, returnType = %s, isBridge() = %s\n", m.getName(), Arrays.toString(m.getParameterTypes()), m.getReturnType(), m.isBridge());
}
}
}
我们看下运行结果:
methodName = play , paramType = [class java.lang.String], returnType = class java.lang.String, isBridge() = false
methodName = play , paramType = [class java.lang.Object], returnType = class java.lang.Object, isBridge() = true
不难发现,它确实存在两个play方法,其中第二个就是编译器生成的桥接方法。
1.3 为什么要生成桥接方法?
前面我们有说到 当子类在继承(或实现)一个带有泛型的父类(或接口)时,在子类中明确指定了泛型,此时编译器在编译时就会自动生成桥接方法
,其实说白了就是和泛型有关。我们知道泛型是JDK5引入了,在JDK5之前,声明一个容器,我们一般会这样:
List list = new ArrayList<>();
list.add("abc");
list.add(123);
list.add(new Object());
list.add(0.3f);
往list容器中可以添加任何类型的对象,当从容器中取数据时,由于不确定类型,所以需要我们手动的去判断所需要的具体类型,在JDK5引入泛型后,我们就可以约定容器只能放什么类型的数据了:
List<String> list = new ArrayList();
list.add("abc");
这样就不用担心类型的问题了。但是泛型是在JDK5引入的,为了向下兼容,引入了泛型擦除的机制,在编译时将泛型去掉,变成Object类型。也正是由于泛型擦除的特性,如果不生成桥接方法,那么就与之前的字节码存在兼容性的问题了。
我们在回过头来看下前面的Aicton
接口的字节码
Compiled from "Action.java"
public interface com.qiuguan.juc.bridge.Action<T extends java.lang.Object>
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Class #10 // com/qiuguan/juc/bridge/Action
#2 = Class #11 // java/lang/Object
#3 = Utf8 play
#4 = Utf8 (Ljava/lang/Object;)Ljava/lang/Object;
#5 = Utf8 Signature
#6 = Utf8 (TT;)TT;
#7 = Utf8 <T:Ljava/lang/Object;>Ljava/lang/Object;
#8 = Utf8 SourceFile
#9 = Utf8 Action.java
#10 = Utf8 com/qiuguan/juc/bridge/Action
#11 = Utf8 java/lang/Object
{
public abstract T play(T);
descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_ABSTRACT
Signature: #6 // (TT;)TT;
}
Signature: #7 // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Action.java"
通过 “Signature: #6” 和 “Signature: #7” 可以看到,在编译完成后实际上就变成了Object类型了
public abstract Object play(Object action)
而Children
实现了这个接口,如果不生成桥接方法,那么Children
就没有实现接口中定义的这个方法,语义就不正确了,所以编译器才会自动生成桥接方法,来保证兼容性。
2.合成方法
我们还是通过例子来看什么是合成方法?,以及什么条件下会生成合成方法?
public class Animal {
public static void main(String[] args) {
Animal.Dog dog = new Animal.Dog();
//外部类访问内部类的私有属性
System.out.println(dog.name);
}
//内部类
private static class Dog {
private String name = "旺财";
}
}
我们将上面的代码编译一下,可以看到有3个文件
Animal$1.class // ?
Animal$Dog.class //内部类
Animal.class //外部类
其中第一个类是做什么的?我们并没有定义过,为什么会产生呢?先带着疑问往下看,我们先看下内部类的反编译结果:
可以使用在线反编译工具,或者用 javap -c Animal\$Dog.class 指令
import com.qiuguan.juc.bridge.Animal.1;
class Animal$Dog {
private String name;
private Animal$Dog() {
this.name = "旺财";
}
//这是一个合成的构造器
// $FF: synthetic method
Animal$Dog(1 x0) {
this();
}
//这里生成了一个 access$100的方法,这个是什么?
// $FF: synthetic method
static String access$100(Animal$Dog x0) {
return x0.name;
}
}
反编译后,我们看到它生成了 access$100的方法,这个方法是干什么的?我们并没有定义呀,为何会生成呢?我们还是继续往下看:
在我上面举的例子中,name
是内部类Dog
的私有属性,但是外部类却直接引用了这个属性,从语法结构上好像没有什么问题,但是从编译器的角度看,这就有点麻烦了,实际上外部类和内部类是平等的,就完全是两个独立的类,这种情况下,外部类直接引用内部类的私有属性,就有点为违背了封装原则。
于是,编译器就要做些什么,我们把外部类反编译也看下
javap -c Animal.class
Compiled from "Animal.java"
public class com.qiuguan.juc.bridge.Animal {
public com.qiuguan.juc.bridge.Animal();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/qiuguan/juc/bridge/Animal$Dog
3: dup
4: aconst_null
5: invokespecial #3 // Method com/qiuguan/juc/bridge/Animal$Dog."<init>":(Lcom/qiuguan/juc/bridge/Animal$1;)V
8: astore_1
9: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_1
//重点看这里。。。。。。
13: invokestatic #5 // Method com/qiuguan/juc/bridge/Animal$Dog.access$100:(Lcom/qiuguan/juc/bridge/Animal$Dog;)Ljava/lang/String;
16: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
19: return
}
重点看第19行的指令,这里在源码中就是输出内部类的name
属性,但是从字节码中我们可以看到,它实际上调用了内部类的 access$100
方法,这个方法是不是比较熟悉了,上面我们刚看到的,这个方法是一个静态方法,它返回的就是内部类的私有属性name
。
现在知道外部类访问内部类的私有属性,编译器为我们做了什么了,接下来我们再继续回过头来看下,编译后生成的第三个类 Animal\$1.class
//看着就是一个普通的类,不过他是编译器生成的合成类。
// $FF: synthetic class
class Animal$1 {
}
这个类看起来就像是一个普通的类,只不过他是编译器生成的一个合成类。
说白了,synthetic 就是突破限制继而能够访问一些private的字段。尤其在这种内部类的情况。
再举一个在日常开发中也比较的枚举
public enum ColorEnum {
RED,BLACK,GREEN,BLUE;
public ColorEnum getColorEnum(String name){
ColorEnum[] values = ColorEnum.values();
for (ColorEnum value : values) {
if (value.name().equals(name)) {
return value;
}
}
return ColorEnum.RED;
}
}
借助在线工具反编译后看下:
public enum ColorEnum {
RED,
BLACK,
GREEN,
BLUE;
// $FF: synthetic field
private static final ColorEnum[] $VALUES = new ColorEnum[]{RED, BLACK, GREEN, BLUE};
public ColorEnum getColorEnum(String name) {
ColorEnum[] values = values();
ColorEnum[] var3 = values;
int var4 = values.length;
for(int var5 = 0; var5 < var4; ++var5) {
ColorEnum value = var3[var5];
if(value.name().equals(name)) {
return value;
}
}
return RED;
}
}
可以看到,它内部会生成一个合成属性
$VALUES。
好了,关于桥接方法和合成方法就记录到这里吧,欢迎大家批评指正,🙋🏻♀️🙋🏻♀️🙋🏻♀️
btw: 桥接方法一定是合成方法,但合成方法不一定是桥接方法。
转载自:https://juejin.cn/post/7240636320762019898