Come conoscere l'uso di Scala.None di Java usando Javap?
-
27-10-2019 - |
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:
None
è un unico oggetto diNone.type
tipo che si estendeOption
- Il
apply()
È richiesto il metodo per l'oggetto compagno
?
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 fX$$anonfun$g$1.class
per gX$$anonfun$h$1$$anonfun$apply$1.class
per HX$$anonfun$i$1.class
eX$$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