¿Cómo se comstackn los rasgos de Scala en bytecode de Java?

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 comstackn los rasgos en bytecode de Java?

Encontré algunas explicaciones breves que los rasgos declarados se comstackn 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 comstackn los rasgos el bytecode de Java?

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

Los rasgos se comstackn 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 comstackdor 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.

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 encoding es que un rasgo sin miembros concretos (que es isomorfo a una interfaz) en realidad se comstack 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 encoding actual podría darnos un poco de compatibilidad hacia adelante: puede usar rasgos comstackdos con un comstackdor nuevo con clases comstackdas por un comstackdor 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á.

Una muy buena explicación de esto es en:

La guía ocupada del desarrollador de Java para Scala: De rasgos y comportamientos – Rasgos en la JVM

Citar:

En este caso, [el comstackdor] descarta las implementaciones del método y las declaraciones de campo definidas en el rasgo en la clase que implementa el rasgo

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

Mejor soporte interno para la encoding de rasgos 2.12

Algunos cambios en la encoding 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) {  } } class C implements T { public int m() { return Tm$(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.