Descompilar el código de Scala: ¿por qué hay dos métodos anulados en la clase derivada?

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
    }
}

La clase B del código anterior se descompila como:

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();
}

No puedo entender por qué hay dos "versiones" del método m1 en el código descompilado. Según tengo entendido, B.m1 simplemente anula A.m1 y public java.lang.Object m1(java.lang.Object) pertenece a A y no debería estar en clase B.

8
CodingNow 25 feb. 2018 a las 01:45

2 respuestas

La mejor respuesta

Este es un método de puente sintético.

En el código de bytes de Java, los métodos solo anulan los métodos con la misma firma exacta. Si B no tuviera ninguna instancia de Object m1(Object), entonces cualquier intento de llamarlo llamaría a la implementación en A, que no es lo que desea. Por lo tanto, el compilador inserta un método de puente sintético que simplemente llama a B m1(Object). Este comportamiento no es específico de Scala, también ocurre en Java puro.

Puede verlo con más detalle examinando el desmontaje. Si compilo y desmonto el siguiente código

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
    }
}

Las partes relevantes de B son

.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 

Como puede ver, el método m1 (Ljava/lang/Object;)Ljava/lang/Object; simplemente envía los argumentos a m1 (Ljava/lang/Object;)LB;.

8
Antimony 24 feb. 2018 a las 23:19

Ves dos métodos porque uno es "el real" y el segundo es un método puente generado para admitir tipos de retorno covariantes .

Desde JavaDoc para Class.getMethod:

mientras que el lenguaje Java prohíbe que una clase declare múltiples métodos con la misma firma pero diferentes tipos de retorno, la máquina virtual Java no lo hace. Esta mayor flexibilidad en la máquina virtual se puede utilizar para implementar varias funciones de lenguaje. Por ejemplo, retornos covariantes se pueden implementar con métodos puente ; el método puente y el método que se anula tendrían la misma firma pero diferentes tipos de retorno.

El método de puente tendrá indicadores ACC_BRIDGE y ACC_SYNTHETICS establecido. En realidad, esto no tiene nada que ver con Scala, como puede ver fácilmente si compila las dos clases siguientes:

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

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

Si ahora usa javap -v para descompilar B.class, verá las diferentes banderas de los métodos:

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
3
Andrey Tyukin 24 feb. 2018 a las 23:42