Domanda

In una domanda precedente, Accesso a Scala.None da Java, sembra che le persone abbiano usato javap Per capire come accedere scala.None da Java. Vorrei sapere come hanno fatto. FYI, la risposta è:

scala.Option$.MODULE$.apply(null);

che può essere corto a:

scala.Option.apply(null);

Dato questo programma (OptionTest.scala):

object OptionTest extends App {
  val x = scala.None
  val y = scala.Some("asdf")
}

Ho corso javap su di esso in questo modo:

javap -s -c -l -private OptionTest

Questa è una parte del javap produzione:

public static final scala.None$ x();
  Signature: ()Lscala/None$;
  Code:
   0:   getstatic  #11; //Field OptionTest$.MODULE$:LOptionTest$;
   3:   invokevirtual  #55; //Method OptionTest$.x:()Lscala/None$;
   6:   areturn

Ho anche corso Javap scala.None e scala.Option. Come si potrebbe capire dal javap Output che:

  1. None è un unico oggetto di None.type tipo che si estende Option
  2. Il apply() È richiesto il metodo per l'oggetto compagno

?

È stato utile?

Soluzione

Ci sono regole su come il codice Scala viene compilato su JVM-Bytecode. A causa dei potenziali scontri di nome, il codice generato non è sempre intuitivo da capire, ma se le regole sono note è possibile accedere al codice Scala compilato all'interno di Java.

Attenzione: Durante la scrittura, ho notato che Javac ed Eclipse-javac si comportano in modo diverso nell'accesso al codice Scala da Java. È possibile che il codice sotto si compili con uno di essi ma non con l'altro.

Classi, costruttori, metodi

Non ci sono regole speciali qui. La seguente classe Scala

class X(i: Int) {
  def m1 = i*2
  def m2(a: Int)(b: Int) = a*b
  def m3(a: Int)(implicit b: Int) = a*b
}

è possibile accedere come una normale classe Java. È compilato in un file denominato X.class:

X x = new X(7);
x.m1();
x.m2(3, 5);
x.m3(3, 5);

Si noti che per i metodi senza un parametro viene creato un parametro vuoto. Più parametri di parametri vengono uniti a uno singolo.

Campi, valori

Per una classe class X(var i: Int) Vengono creati getter e setter. Per una classe class X(val i: Int) Viene creato solo un getter:

//Scala
val x = new X(5)
x.i = 3 // Setter
x.i // Getter

//Java
X x = new X(5);
x.i_$eq(3); // Setter
x.i(); // Getter

Si noti che in Java un identificatore non è autorizzato a includere segni speciali. Pertanto Scalac genera per ciascuno di questi segni speciali un nome specifico. C'è una classe Scala.reflect.NameTransformer che può codificare/decodificare le operazioni:

scala> import scala.reflect.NameTransformer._
import scala.reflect.NameTransformer._

scala> val ops = "~=<>!#%^&|*/+-:\\?@"
ops: String = ~=<>!#%^&|*/+-:\?@

scala> ops map { o => o -> encode(o.toString) } foreach println
(~,$tilde)
(=,$eq)
(<,$less)
(>,$greater)
(!,$bang)
(#,$hash)
(%,$percent)
(^,$up)
(&,$amp)
(|,$bar)
(*,$times)
(/,$div)
(+,$plus)
(-,$minus)
(:,$colon)
(\,$bslash)
(?,$qmark)
(@,$at)

Una classe class X { var i = 5 } è tradotto dallo stesso schema di quando il campo viene creato nel costruttore. Accesso diretto alla variabile i Da Java non è possibile, perché è privato.

Oggetti

Non esiste un oggetto Scala in Java. Pertanto Scalac deve fare un po 'di magia. Per un oggetto object X { val i = 5 } Vengono generati due file di classe JVM: X.class e X$.class. Il primo funziona come un'interfaccia, include metodi statici per accedere ai campi e ai metodi dell'oggetto Scala. Quest'ultima è una classe singleton che non può essere istanziata. Ha un campo che detiene l'istanza singleton della classe, chiamata MODULE$, che consente l'accesso al singleton:

X.i();
X$.MODULE$.i();

Classi di casi

Il compilatore Scala genera automaticamente un metodo applicato per una classe di casi e getter per i campi. La classe del caso case class X(i: Int) è facilmente accessibile:

new X(3).i();
X$.MODULE$.apply(3);

Tratti

Un tratto trait T { def m }, che contiene solo membri astratti, viene compilato in un'interfaccia, che viene inserita in un file di classe denominato T.class. Pertanto può facilmente implementare da una classe Java:

class X implements T {
  public void m() {
    // do stuff here
  }
}

Se il tratto contiene membri in cemento, c'è un file di classe denominato <trait_name>$class.class generato, oltre all'interfaccia normale. Il tratto

trait T {
  def m1
  def m2 = 5
}

può anche facilmente implementare all'interno di Java. Il file di classe T$class.class Contiene i membri concreti del tratto, ma sembra che siano impossibili accedere a Java. Né Javac né Eclipse-javac compileranno un accesso a questa classe.

È possibile trovare alcuni dettagli su come si possono trovare i tratti qui.

Funzioni

I letterali delle funzioni sono compilati come istanze anonime della funzione delle classi. Un oggetto Scala

object X {
  val f: Int => Int = i => i*2
  def g: Int => Int = i => i*2
  def h: Int => Int => Int = a => b => a*b
  def i: Int => Int => Int = a => {
    def j: Int => Int = b => a*b
    j
  }
}

è compilato nei normali file di classe, come descritto sopra. Inoltre ogni funzione letterale ottiene il proprio file di classe. Quindi, per i valori delle funzioni un file di classe denominato <class_name>$$anonfun$<N>.class è generato, dove n è un numero continuo. Per metodi di funzione (metodi, che restituiscono una funzione) un file di classe denominato <class_name>$$anonfun$<method_name>$<N>.class è generato. Le parti del nome della funzione sono separate da segni in dollari e davanti al anonfun Identificatore Ci sono anche due segni di dollari. Per le funzioni nidificate il nome della funzione nidificata è aggiunto alla funzione esterna, ciò significa che una funzione interiore otterrà un file di classe come <class_name>$$anonfun$<outer_method_name>$<N>$$anonfun$<inner_method_name>$<N>.class. Quando una funzione interiore non ha un nome, come visto in h ottiene il nome apply.

Questo significa che nel nostro caso otteniamo:

  • X$$anonfun$1.class per f
  • X$$anonfun$g$1.class per g
  • X$$anonfun$h$1$$anonfun$apply$1.class per H
  • X$$anonfun$i$1.class e X$$anonfun$i$1$$anonfun$j$1$1.class per io e j

Per accedervi, usa il loro metodo applicato:

X.f().apply(7);
X.g().apply(7);
X.h().apply(3).apply(5);
X.i().apply(3).apply(5);

Rispondi alla domanda

Dovresti sapere:

  • Una normale classe Scala può accedere ai loro costruttori o ai loro metodi applicabili
  • Quando non c'è costruttore di quanto non ci sia un metodo applicato
  • Quando non esiste un costruttore e nessun metodo applicabile di un altro file di classe chiamato allo stesso modo in cui viene chiamata la classe che aggiunge un cartello in dollari alla fine. Cerca in questa classe a MODULE$ campo
  • I costruttori e i metodi applicabili sono ereditati, quindi cerca le super-Classi se non riesci a trovare nulla nelle sottoclassi

Qualche esempio

Opzione

// javap scala.Option
public abstract class scala.Option extends java.lang.Object implements ... {
  ...
  public static final scala.Option apply(java.lang.Object);
  public scala.Option();
}

Javap afferma di avere un costruttore e un metodo applicato. Inoltre, afferma che la classe è astratta. Pertanto solo il metodo applicato può essere utilizzato:

Option.apply(3);

Alcuni

// javap scala.Some
public final class scala.Some extends scala.Option implements ... {
  ...
  public scala.Some(java.lang.Object);
}

Ha un costruttore e un metodo applicato (perché sappiamo che l'opzione ha un'opzione e alcune estendi). Usa uno di loro e sii felice:

new Some<Integer>(3);
Some.apply(3);

Nessuno

// javap scala.None
public final class scala.None extends java.lang.Object{
  ...
}

Non ha costruttore, nessun metodo applicato e non estende l'opzione. Quindi, daremo un'occhiata a None$:

// javap -private scala.None$
public final class scala.None$ extends scala.Option implements ... {
  ...
  public static final scala.None$ MODULE$;
  private scala.None$();
}

Sì! Abbiamo trovato un MODULE$ campo e il metodo applicato dell'opzione. Inoltre abbiamo trovato il costruttore privato:

None$.apply(3) // returns Some(3). Please use the apply-method of Option instead
None$.MODULE$.isDefined(); // returns false
new None$(); // compiler error. constructor not visible

Elenco

scala.collection.immutable.List è astratto, quindi dobbiamo usare scala.collection.immutable.List$. Ha un metodo applicato che si aspetta un scala.collection.Seq. Quindi, per ottenere un elenco, abbiamo bisogno di prima una seq. Ma se guardiamo a seq non c'è metodo applicato. Inoltre, quando guardiamo i super-Classi di SEQ e AT scala.collection.Seq$ Possiamo solo trovare un metodo applicato che si aspetta una seq. Quindi che si fa?

Dobbiamo dare un'occhiata a come Scalac crea un'istanza di elenco o seq. Prima crea una classe Scala:

class X {
  val xs = List(1, 2, 3)
}

Compilalo con Scalac e guarda il file di classe con Javap:

// javap -c -private X
public class X extends java.lang.Object implements scala.ScalaObject{
...
public X();
  Code:
   0:   aload_0
   1:   invokespecial   #20; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   getstatic   #26; //Field scala/collection/immutable/List$.MODULE$:Lscala/collection/immutable/List$;
   8:   getstatic   #31; //Field scala/Predef$.MODULE$:Lscala/Predef$;
   11:  iconst_3
   12:  newarray int
   14:  dup
   15:  iconst_0
   16:  iconst_1
   17:  iastore
   18:  dup
   19:  iconst_1
   20:  iconst_2
   21:  iastore
   22:  dup
   23:  iconst_2
   24:  iconst_3
   25:  iastore
   26:  invokevirtual   #35; //Method scala/Predef$.wrapIntArray:([I)Lscala/collection/mutable/WrappedArray;
   29:  invokevirtual   #39; //Method scala/collection/immutable/List$.apply:(Lscala/collection/Seq;)Lscala/collection/immutable/List;
   32:  putfield    #13; //Field xs:Lscala/collection/immutable/List;
   35:  return

}

Il costruttore è interessante. Ci dice che viene creata una serie di INT (l. 12) che è riempito con 1, 2 e 3. (l. 14-25). Dopodiché questo array viene consegnato scala.Predef$.wrapIntArray (l. 26). Questo risultante scala.collection.mutable.WrappedArray viene nuovamente consegnato al nostro elenco (l. 29). Alla fine, l'elenco è archiviato nel campo (l. 32). Quando vogliamo creare un elenco in Java, dobbiamo fare lo stesso:

int[] arr = { 1, 2, 3 };
WrappedArray<Object> warr = Predef$.MODULE$.wrapIntArray(arr);
List$.MODULE$.apply(warr);

// or shorter
List$.MODULE$.apply(Predef$.MODULE$.wrapIntArray(new int[] { 1, 2, 3 }));

Sembra brutto, ma funziona. Se crei una bella libreria che avvolge l'accesso alla libreria Scala, sarà facile da usare Scala da Java.

Riepilogo

So che ci sono altre regole su come il codice Scala viene compilato su Bytecode. Ma penso che con le informazioni sopra dovrebbe essere possibile trovare queste regole da solo.

Altri suggerimenti

Non sono in competizione con l'altra risposta, ma dal momento che le persone sembrano spesso non notare questo, puoi farlo nella repl.

scala> :paste
// Entering paste mode (ctrl-D to finish)

object OptionTest extends App {
  val x = scala.None
  val y = scala.Some("asdf")
}

// Exiting paste mode, now interpreting.

defined module OptionTest

scala> :javap -v OptionTest$
Compiled from "<console>"
public final class OptionTest$ extends java.lang.Object implements scala.App,scala.ScalaObject
  SourceFile: "<console>"
  Scala: length = 0x

  [lots of output etc]   

  public scala.None$ x();
    Code:
     Stack=1, Locals=1, Args_size=1
     0: aload_0
     1: getfield    #65; //Field x:Lscala/None$;
     4: areturn
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top