8

反编译Scala代码:为什么派生类中有两个被覆盖的方法?

class A
{
    private var str: String = "A"
    val x: A = this

    override def toString(): String = str

    def m1(other: AnyRef): AnyRef = {
      println("This is A.m1(AnyRef)")
      other
    }
}

class B extends A {
    private var str: String = "B"
    var z: Int = 0
    override val x: B = this

    override def m1(other: AnyRef): B = {
      println("This is B.m1(AnyRef)")
      this
    }
}

上面代码的class B被反编译为:

public class test$B extends test$A {
  private java.lang.String str;
  private int z;
  private final test$B x;
  private java.lang.String str();
  private void str_$eq(java.lang.String);
  public int z();
  public void z_$eq(int);
  public test$B x();
  public test$B m1(java.lang.Object);
  public java.lang.Object m1(java.lang.Object);
  public test$A x();
  public test$B();
}

我不明白为什么m1反编译的代码中有两个“版本”的方法。据我了解,B.m1只是覆盖A.m1public java.lang.Object m1(java.lang.Object)属于A并且不应该在 class 中B

4

2 回答 2

8

这是一种合成桥接法。

在 Java 字节码中,方法只覆盖具有完全相同签名的方法。如果 B 没有任何实例Object m1(Object),那么任何调用它的尝试都会调用 A 中的实现,这不是你想要的。因此,编译器插入了一个合成桥方法,它简单地调用B m1(Object). 这种行为并不是 Scala 特有的——它也发生在纯 Java 中。

您可以通过检查反汇编来更详细地查看它。如果我编译和反汇编以下代码

class A
{
    def m1(other: AnyRef): AnyRef = {
      println("This is A.m1(AnyRef)")
      other
    }
}

class B extends A {
    override def m1(other: AnyRef): B = {
      println("This is B.m1(AnyRef)")
      this
    }
}

B的相关部分是

.method public m1 : (Ljava/lang/Object;)LB; 
    .code stack 2 locals 2 
L0:     getstatic Field scala/Predef$ MODULE$ Lscala/Predef$; 
L3:     ldc 'This is B.m1(AnyRef)' 
L5:     invokevirtual Method scala/Predef$ println (Ljava/lang/Object;)V 
L8:     aload_0 
L9:     areturn 
L10:    
    .end code 
    .methodparameters 
        other final 
    .end methodparameters 
.end method 

.method public bridge synthetic m1 : (Ljava/lang/Object;)Ljava/lang/Object; 
    .code stack 2 locals 2 
L0:     aload_0 
L1:     aload_1 
L2:     invokevirtual Method B m1 (Ljava/lang/Object;)LB; 
L5:     areturn 
L6:     
    .end code 
    .methodparameters 
        other final 
    .end methodparameters 
.end method 

如您所见,该方法m1 (Ljava/lang/Object;)Ljava/lang/Object;只是将参数转发给m1 (Ljava/lang/Object;)LB;.

于 2018-02-24T23:19:41.753 回答
3

您会看到两种方法,因为一种是“真正的”,第二种是为支持协变返回类型而生成的桥接方法

来自Class.getMethod 的 JavaDoc

Java 语言禁止一个类声明具有相同签名但返回类型不同的多个方法,而 Java 虚拟机则不会。虚拟机中这种增加的灵活性可用于实现各种语言功能。例如,协变返回可以用桥接方法来实现;桥接方法和被覆盖的方法将具有相同的签名,但返回类型不同。

桥接方法将设置标志ACC_BRIDGE 和 ACC_SYNTHETICS。这实际上与 Scala 无关,因为您可以很容易地看到您是否编译了以下两个类:

class A {
  public Object m1(int i) { return i; }
}

class B extends A {
  @Override public String m1(int a) { return "hey " + a; }
}

如果您现在使用javap -v反编译B.class,您将看到方法的不同标志:

public java.lang.String m1(int);
  descriptor: (I)Ljava/lang/String;
  flags: ACC_PUBLIC

[...some lines omitted...]

public java.lang.Object m1(int);
  descriptor: (I)Ljava/lang/Object;
  flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
于 2018-02-24T23:42:50.193 回答