Come vengono compilati i tratti Scala nel bytecode Java?

Ho giocato un po ‘con Scala per un po’ e so che i tratti possono essere l’equivalente Scala di entrambe le interfacce e classi astratte. In che modo esattamente i caratteri sono compilati in bytecode Java?

Ho trovato alcune brevi spiegazioni in cui i tratti dichiarati sono compilati esattamente come le interfacce Java quando ansible e si interfacciano con un’altra class altrimenti. Continuo a non capire, comunque, come Scala abbia raggiunto la linearizzazione della class, una funzionalità non disponibile in Java.

C’è una buona fonte che spiega come i caratteri vengono compilati in bytecode Java?

Non sono un esperto, ma qui è la mia comprensione:

I tratti sono compilati in un’interfaccia e una class corrispondente.

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

diventa l’equivalente di …

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

Il che lascia la domanda: come viene chiamato il metodo della barra statica nella class Foo $? Questa magia viene eseguita dal compilatore nella class in cui viene mescolato il carattere Foo.

 class Baz extends Foo 

diventa qualcosa come …

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

La linearizzazione della class implementa semplicemente la versione appropriata del metodo (chiamando il metodo statico nella class di class Xxxx $) in base alle regole di linearizzazione definite nelle specifiche del linguaggio.

Per ragioni di discussione, diamo un’occhiata al seguente esempio di Scala che usa tratti multipli con metodi sia astratti che concreti:

 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) = ??? } 

Al momento (ad esempio in Scala 2.11), un singolo tratto è codificato come:

  • interface contenente dichiarazioni astratte per tutti i metodi del tratto (sia astratti che concreti)
  • una class statica astratta contenente metodi statici per tutti i metodi concreti del trait, prendendo un parametro extra $this (nelle versioni precedenti di Scala, questa class non era astratta, ma non ha senso istanziarla)
  • in ogni punto della gerarchia dell’ereditarietà in cui il tratto è mescolato, i metodi di inoltro sintetico per tutti i metodi concreti nel tratto che inoltra ai metodi statici della class statica

Il vantaggio principale di questa codifica è che un tratto senza membri concreti (che è isomorfo per un’interfaccia) in realtà è compilato su un’interfaccia.

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

Tuttavia, Scala 2.12 richiede Java 8 e quindi è in grado di utilizzare metodi e metodi statici predefiniti nelle interfacce, e il risultato è più simile a questo:

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

Come puoi vedere, il vecchio design con i metodi statici e gli spedizionieri è stato mantenuto, sono semplicemente piegati nell’interfaccia. I metodi concreti del tratto sono ora stati spostati nell’interfaccia stessa come metodi static , i metodi di inoltro non sono sintetizzati in ogni class ma definiti una volta come metodi default e il metodo $init$ statico (che rappresenta il codice nel corpo del tratto) è stato spostato nell’interfaccia, rendendo superflua la class statica del compagno.

Potrebbe probabilmente essere semplificato in questo modo:

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

Non sono sicuro del motivo per cui non è stato fatto. A prima vista, la codifica attuale potrebbe darci un po ‘di compatibilità con i forwards: puoi usare tratti compilati con un nuovo compilatore con classi compilate da un vecchio compilatore, quelle vecchie sostituiranno semplicemente i metodi di forwarder default che ereditano dall’interfaccia con quelli identici. Tranne che i metodi di inoltro cercheranno di chiamare i metodi statici su A$class e B$class che non esistono più, in modo tale che l’ipotetica compatibilità con i forwards non funzioni effettivamente.

Una buona spiegazione di questo è in:

L’indaffaratissima guida per sviluppatori Java di Scala: di tratti e comportamenti – Tratti nella JVM

Citazione:

In questo caso, [il compilatore] elimina le implementazioni del metodo e le dichiarazioni di campo definite nel tratto nella class che implementa il tratto

Nel contesto di Scala 12 e Java 8, puoi vedere un’altra spiegazione nel commit 8020cd6 :

Migliore supporto per inliner per la codifica dei caratteri da 2.12

Alcune modifiche alla codifica dei tratti sono arrivate tardi nel ciclo 2.12 e l’inliner non è stato adattato per supportarlo nel miglior modo ansible.

In 2.12.0 i metodi di tratto concreto sono codificati come

 interface T { default int m() { return 1 } static int m$(T $this) {  } } class C implements T { public int m() { return Tm$(this) } } 

Se un metodo tratto è selezionato per l’inlining, l’inliner 2.12.0 copierà il suo corpo nel super accessor statico Tm$ , e da lì nel forwarder mixin Cm .

Questo commette casi speciali all’inliner:

  • Non siamo in linea con i superaccessori statici e mescoliamo i forwarder.
  • Invece, quando si insinua un’invocazione di un forwarder mixin, l’inliner segue anche i due forwarder e allinea il corpo del metodo dei tratti.