桥接方法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: 桥接办法一定是组成办法,但组成办法不一定是桥接办法。