Java 桥接方法

Java 桥接方法

桥接方法概念

Java中的桥接方法(Bridge Method)是一种为了实现某些Java语言特性而由编译器自动生成的方法。可以通过使用Java反射中 Method 类的 isBridge() 方法来判断该方法是否是桥接方法。通过反射 Class.getMethod("") 取出的不是桥接方法。

在字节码文件中,桥接方法会被标记为 ACC_BRIDGE 和 ACC_SYNTHETIC,其中 ACC_BRIDGE 表示该方法是由编译器产生的桥接方法, ACC_SYNTHETIC 表示该方法是由编译器自动生成。

什么情况下会生成桥接方法

协变返回类型

协变返回类型是指子类方法的返回值类型不必严格等同于父类中被重写的方法的返回值类型,而可以是更具体的类型,即子类重写父类方法时,返回的类型可以是子类方法返回类型的子类。

看如下代码:

public class Parent {
    public Number get(){
        return 0;
    }
}

class Child1 extends Parent {

    public Number get(){
        return 1;
    }
}

class Child2 extends Parent {

    public Integer get(){
        return 2;
    }
}

运行以下命令,编译并查看字节码数据:

javac Parent.java
javap -c -v Child1
javap -c -v Child2

显示结果如下(省略不重要的部分代码):

{
  Child1();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Parent."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0

  public java.lang.Number get();
    descriptor: ()Ljava/lang/Number;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_1
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 12: 0
}
{
  Child2();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Parent."<init>":()V
         4: return
      LineNumberTable:
        line 16: 0

  public java.lang.Integer get();
    descriptor: ()Ljava/lang/Integer;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_2
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 19: 0

  public java.lang.Number get();
    descriptor: ()Ljava/lang/Number;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #3                  // Method get:()Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 16: 0
}

对比一下 Child1 和 Child2 的字节码可以看出,Child2 中多了一个 public java.lang.Number get(); 方法, 这个方法就是桥接方法,可以通过该方法的 flags 看到它含有 ACC_BRIDGE 标志。这个方法就是一个桥接作用,从第30行可以看出它的方法内容,就是通过 invokevirtual 调用了自身的 get() 方法。为什么需要通过这种方式来调用呢,是因为在JVM来说一个方法的签名比Java语言多了一个返回值类型,也就是说,在Java语言中,认为只要方法名和参数列表一致就是同一个方法,而JVM则认为方法名、参数列表和返回类型全部一样才是同一方法。而桥接方法的签名和其父类的方法签名一致,以此就实现了协变返回值类型。

泛型类型擦除

泛型是Java 1.5才引进的概念,在这之前是没有泛型的概念的,但泛型代码能够很好地和之前版本的代码很好地兼容,这是因为,在编译期间Java编译器会将类型参数替换为其上界(类型参数中extends子句的类型),如果上界没有定义,则默认为Object,这就叫做类型擦除。当一个子类在继承(或实现)一个父类(或接口)的泛型方法时,在子类中明确指定了泛型类型,那么在编译时编译器会自动生成桥接方法。

看如下代码:

public class Parent<T> {
    public Number get(T key){
        return 0;
    }
}

class Child1 extends Parent<String> {

    public Number get(String key){
        return 1;
    }
}

class Child2 extends Parent<String> {

    public Integer get(String key){
        return 2;
    }
}

运行以下命令,编译并查看字节码数据:

javac Parent.java
javap -c -v Child1
javap -c -v Child2

显示结果如下(省略不重要的部分代码):

{
  Child1();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Parent."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0

  public java.lang.Number get(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/Number;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iconst_1
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 12: 0

  public java.lang.Number get(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Number;
    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 get:(Ljava/lang/String;)Ljava/lang/Number;
         8: areturn
      LineNumberTable:
        line 9: 0
}
{
  Child2();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Parent."<init>":()V
         4: return
      LineNumberTable:
        line 16: 0

  public java.lang.Integer get(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/Integer;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iconst_2
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 19: 0

  public java.lang.Number get(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Number;
    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 get:(Ljava/lang/String;)Ljava/lang/Integer;
         8: areturn
      LineNumberTable:
        line 16: 0
}

对比一下 Child1 和 Child2 的字节码可以看出,Child2 中多了一个 public java.lang.Number get(java.lang.Object); 方法, 这个方法就是桥接方法,可以通过该方法的 flags 看到它含有 ACC_BRIDGE 标志。还可以看出这个桥接方法的参数类型是 Object 类型,它与父类的参数类型是一致的,包括返回值也是与父类的类型是一致的。它的方法内容就是先进行类型检查(第31行),然后再通过 invokevirtual 指令调用自身的 get(java.lang.String); 方法。

对于JVM来说,当它编译时,它会直接把类型进行擦除,也就是会把类 Parent 变成如下形式:

public class Parent<Object> {
    public Number get(Object key){
        return 0;
    }
}

查询桥接方法

使用 spring 提供的查找方式:

method = BridgeMethodResolver.findBridgedMethod(method);
原文地址:https://www.cnblogs.com/dwtfukgv/p/14887575.html