ejemplos caracteristicas scala bytecode

caracteristicas - ¿Cómo se compilan los rasgos de Scala en bytecode de Java?



bytecode java ejemplos (4)

He jugado con Scala durante un tiempo, y sé que los rasgos pueden actuar como el equivalente de Scala de ambas interfaces y clases abstractas. ¿Cómo se compilan los rasgos en bytecode de Java?

Encontré algunas explicaciones breves que los rasgos declarados se compilan exactamente como las interfaces de Java cuando sea posible, y las interfaces con una clase adicional de lo contrario. Todavía no entiendo, sin embargo, cómo Scala logra la linealización de clase, una característica no disponible en Java.

¿Hay alguna buena fuente que explique cómo compilan los rasgos el bytecode de Java?


En aras de la discusión, veamos el siguiente ejemplo de Scala usando múltiples rasgos con métodos abstractos y concretos:

trait A { def foo(i: Int) = ??? def abstractBar(i: Int): Int } trait B { def baz(i: Int) = ??? } class C extends A with B { override def abstractBar(i: Int) = ??? }

Por el momento (es decir, a partir de Scala 2.11), un único rasgo se codifica como:

  • una interface contiene declaraciones abstractas para todos los métodos del rasgo (tanto abstractos como concretos)
  • una clase estática abstracta que contiene métodos estáticos para todos los métodos concretos del rasgo, tomando un parámetro extra $this (en versiones anteriores de Scala, esta clase no era abstracta, pero no tiene sentido crear una instancia)
  • en cada punto de la jerarquía de herencia donde se mezcla el rasgo, métodos de reenvío sintético para todos los métodos concretos en el rasgo que reenvían a los métodos estáticos de la clase estática

La principal ventaja de esta codificación es que un rasgo sin miembros concretos (que es isomorfo a una interfaz) en realidad se compila en una interfaz.

interface A { int foo(int i); int abstractBar(int i); } abstract class A$class { static void $init$(A $this) {} static int foo(A $this, int i) { return ???; } } interface B { int baz(int i); } abstract class B$class { static void $init$(B $this) {} static int baz(B $this, int i) { return ???; } } class C implements A, B { public C() { A$class.$init$(this); B$class.$init$(this); } @Override public int baz(int i) { return B$class.baz(this, i); } @Override public int foo(int i) { return A$class.foo(this, i); } @Override public int abstractBar(int i) { return ???; } }

Sin embargo, Scala 2.12 requiere Java 8 y, por lo tanto, puede usar métodos predeterminados y métodos estáticos en las interfaces, y el resultado se parece más a esto:

interface A { static void $init$(A $this) {} static int foo$(A $this, int i) { return ???; } default int foo(int i) { return A.foo$(this, i); }; int abstractBar(int i); } interface B { static void $init$(B $this) {} static int baz$(B $this, int i) { return ???; } default int baz(int i) { return B.baz$(this, i); } } class C implements A, B { public C() { A.$init$(this); B.$init$(this); } @Override public int abstractBar(int i) { return ???; } }

Como puede ver, el diseño anterior con los métodos estáticos y reenviadores se ha conservado, simplemente se han plegado en la interfaz. Los métodos concretos del rasgo ahora se han trasladado a la interfaz en sí como métodos static , los métodos del promotor no se sintetizan en todas las clases pero se definen una vez como métodos default y el método estático $init$ (que representa el código en el cuerpo del rasgo) se ha movido a la interfaz también, haciendo innecesaria la clase estática acompañante.

Probablemente podría simplificarse así:

interface A { static void $init$(A $this) {} default int foo(int i) { return ???; }; int abstractBar(int i); } interface B { static void $init$(B $this) {} default int baz(int i) { return ???; } } class C implements A, B { public C() { A.$init$(this); B.$init$(this); } @Override public int abstractBar(int i) { return ???; } }

No estoy seguro de por qué esto no se hizo. A primera vista, la codificación actual podría darnos un poco de compatibilidad hacia adelante: puede usar rasgos compilados con un compilador nuevo con clases compiladas por un compilador antiguo, esas viejas clases simplemente anularán los métodos de reenvío default que heredan de la interfaz con idénticos. Excepto que los métodos de reenvío intentarán llamar a los métodos estáticos en la A$class y en la A$class B$class que ya no existan, por lo que la compatibilidad hipotética con los reenvíos en realidad no funcionará.


En el contexto de Scala 12 y Java 8, puede ver otra explicación en commit 8020cd6 :

Mejor soporte interno para la codificación de rasgos 2.12

Algunos cambios en la codificación del rasgo llegaron tarde en el ciclo 2.12, y el interno no se adaptó para soportarlo de la mejor manera posible.

En 2.12.0, los métodos de rasgos concretos están codificados como

interface T { default int m() { return 1 } static int m$(T $this) { <invokespecial $this.m()> } } class C implements T { public int m() { return T.m$(this) } }

Si se selecciona un método de rasgo para enlining, el inliner 2.12.0 copiará su cuerpo en el superaccesador estático Tm$ , y desde allí en el forwarder mixin Cm .

Esto compromete casos especiales el inliner:

  • No nos alineamos en super accessors estáticos y forwarders mixin.
  • En cambio, cuando se inline una invocación de un forwarder de mixin, el inliner también sigue a través de los dos reenviadores y enmarca el cuerpo del método de rasgo.

No soy un experto, pero aquí está mi entendimiento:

Los rasgos se compilan en una interfaz y clase correspondiente.

trait Foo { def bar = { println("bar!") } }

se convierte en el equivalente de ...

public interface Foo { public void bar(); } public class Foo$class { public static void bar(Foo self) { println("bar!"); } }

Lo que deja la pregunta: ¿cómo se llama el método de barra estática en Foo $ class? Esta magia la realiza el compilador de la clase en la que se mezcla el rasgo de Foo.

class Baz extends Foo

se convierte en algo así como ...

public class Baz implements Foo { public void bar() { Foo$class.bar(this); } }

La linealización de clase simplemente implementa la versión apropiada del método (llamando al método estático en la clase Xxxx $ class) de acuerdo con las reglas de linealización definidas en la especificación del lenguaje.