Java桥接方法

221 阅读6分钟

Java中的桥接方法是一种合成方法,在实现某些Java语言特性的时候是很有必要的。最为人熟知的例子就是协变返回值类型泛型擦除后导致的基类方法的参数与实际调用的方法参数类型不一致以及**“改变”基类可见性**。

协变返回值类型

泛型擦除

看以下的例子

public class Foo<T> {

    public T get(){
        return null;
    }
}

public class Bar extends Foo<string> {

    @Override
    public String get() {
        return null;
    }
}

如果你知道泛型擦除,那么就应该知道,泛型是在JDK1.5引入,它只在编译期起作用,所以上面的代码在泛型擦除之后,预期会变成这样:

public class Foo {

    public Object get(){
        return null;
    }
}

public class Bar extends Foo {

    @Override
    public String get() {
        return null;
    }
}

简单的说,泛型T被擦除了,而在方法或字段中引用的泛型变成了Object

看起来没有问题,也完全符合方法重写的规则,但是,我们将FooBar使用Javap -v编译为字节码,内容如下:

编译Foo.class

# javap -v Foo.class
  ......
  public T get();
    descriptor: ()Ljava/lang/Object;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aconst_null
         1: areturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       2     0  this   Lcom/learn/java/bradgemetod/Foo;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       2     0  this   Lcom/learn/java/bradgemetod/Foo<TT;>;
    Signature: #16                          // ()TT;
}
Signature: #17                          // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Foo.java" 

确实如我们所预期的那样,T变为了java/lang/Object

编译Bar.class

# javap -v Bar.class
  ......
  public java.lang.String get();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aconst_null
         1: areturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       2     0  this   Lcom/learn/java/bradgemetod/Bar;

  public java.lang.Object get();
    descriptor: ()Ljava/lang/Object;
    flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method get:()Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/learn/java/bradgemetod/Bar;
}
Signature: #16                          // Lcom/learn/java/bradgemetod/Foo<Ljava/lang/String;>;
SourceFile: "Bar.java"

看起来出乎意料,JVM合成了一个新的方法public java.lang.Object get(),这在源代码中是没有出现过的,这个新增的方法被标记为ACC_BRIDGEACC_SYNTHETIC,并且其转调了public String get()方法,它起了一个桥接的作用,编译器不得不这么做,因为在JVM方法中,返回类型也是方法签名的一部分,而桥接方法的创建就正好是实现协变返回值类型的方式。

ACC_BRIDGE:这个方法是由编译生成的桥接方法。

ACC_SYNTHETIC:这个方法是由编译器生成,并且不会在源代码中出现。

桥接方法测试

public static void main(String[] args) throws NoSuchMethodException {
    Class<Bar> barClass = Bar.class;
    for (Method method : barClass.getDeclaredMethods()) {
        System.out.println(method.getReturnType().getName()
                           +" "+method.getName()
                           +"() is bridge method : "+method.isBridge());
    }
}

//控制台输出:
//java.lang.String get() is bridge method : false
//java.lang.Object get() is bridge method : true

泛型擦除入参类型不一致

看一下另一个代码示例,同样的编译源码:

public class Foo<T> {

    public void set(T argument) {
        
    }
}

public class Bar extends Foo<String> {

    @Override
    public void set(String argument) {

    }
}

编译Foo.class

# javap -v Foo.class
  ......
  public void set(T);
    descriptor: (Ljava/lang/Object;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/learn/java/bradgemetod/Foo;
            0       1     1 argument   Ljava/lang/Object;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/learn/java/bradgemetod/Foo<TT;>;
            0       1     1 argument   TT;
    Signature: #19                          // (TT;)V
}
Signature: #20                          // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Foo.java"

编译Bar.class

# javap -v Bar.class
  ......
  public void set(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/learn/java/bradgemetod/Bar;
            0       1     1 argument   Ljava/lang/String;

  public void set(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #2                  // class java/lang/String
         5: invokevirtual #3                  // Method set:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/learn/java/bradgemetod/Bar;
}
Signature: #19                          // Lcom/learn/java/bradgemetod/Foo<Ljava/lang/String;>;
SourceFile: "Bar.java"

从上面的代码示例以及反编译结果看起来与协变返回值类型情况没有差别,这里的桥接方法覆盖了基类的set方法,不仅使用字符串参数将对自身的调用委派给字符串参数的set方法,还执行了一个到java.lang.String的类型转换检测(#2),这意味着,如果你执行如下代码,并忽略编译器的警告,将会从桥接方法哪里抛出ClassCastException异常。

public static void main(String[] args) {
    Foo foo = new Bar();
    foo.set(new Object());
}

输出结果:

Exception in thread "main" java.lang.ClassCastException: java.base/java.lang.Object cannot be cast to java.base/java.lang.String
	at com.learn.java.bradgemetod.Bar.set(Bar.java:3)
	at com.learn.java.bradgemetod.BradgeMethodTest.main(BradgeMethodTest.java:7)

桥接方法测试

public static void main(String[] args) throws NoSuchMethodException {
    Class<Bar> barClass = Bar.class;
    Method setObject = barClass.getMethod("set", Object.class);
    System.out.println("set(Object) is bridge method : "+setObject.isBridge());
    Method setString = barClass.getMethod("set", String.class);
    System.out.println("set(String) is bridge method : "+setString.isBridge());
}

//控制台输出:
//set(Object) is bridge method : true
//set(String) is bridge method : false

“改变”基类可见性

另外一种桥接方式是由于基类可见性问题引起的,参考如下示例:

编写Foo、Bar1、Bar2类,示例如下:

class Foo {

    public void set(){

    }
}

public class Bar1 extends Foo {

    @Override
    public void set() {

    }
}

public class Bar2 extends Foo {

}

编译Foo.class

  public void set();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/learn/java/bradgemetod/Foo;
}
SourceFile: "Foo.java"

编译Bar1.class

  public void set();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/learn/java/bradgemetod/Bar1;
}
SourceFile: "Bar1.java"

编译Bar2.class

  public void set();
    descriptor: ()V
    flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method com/learn/java/bradgemetod/Foo.set:()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/learn/java/bradgemetod/Bar2;
}
SourceFile: "Bar2.java"

Foobar1的字节码看起来平平无奇,跟源码一致,但是编译bar2,会发现其字节码中生成了桥接方法set(),并被标记为ACC_BRIDGEACC_SYNTHETIC

编译器需要这样的方法,因为Foo类不是公开的,在Foo类所在包之外是不可见的,但是bar2类是公开的,它所继承来的所有方法在所在包之外都是可见的。需要注意的是,bar1类不会有桥接方法生成,因为它覆盖了set方法,因此没有必要“提升”其可见性。

虽然是生成了桥接方法,但是真的是因为Foo类不是公开的,在Foo类所在包之外是不可见的吗?为了验证这个问题,将Foo类修改为public,然后再次编译bar2类,编译后字节码如下:

  public com.learn.java.bradgemetod.Bar2();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method com/learn/java/bradgemetod/Foo."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/learn/java/bradgemetod/Bar2;
}
SourceFile: "Bar2.java"

可以看到,在Foo类为public的情况下,bar2并没有生成桥接方法。

桥接方法测试

public static void main(String[] args) throws NoSuchMethodException {
        System.out.println("Bar1 set() is bridge method : "+
                           Bar1.class.getMethod("set").isBridge());
        System.out.println("Bar2 set() is bridge method : "+
                           Bar2.class.getMethod("set").isBridge());
}

//控制台输出:
//Bar1 set() is bridge method : false
//Bar2 set() is bridge method : true

引用https://www.jianshu.com/p/2eaf4d5c168d