¿Cómo aprender sobre el uso de Scala.None de Java usando Javap?
-
27-10-2019 - |
Pregunta
En una pregunta anterior, Acceso a Scala.None desde Java, parece que la gente había usado javap
para descubrir cómo acceder scala.None
de Java. Me gustaría saber cómo hicieron eso. FYI, la respuesta es:
scala.Option$.MODULE$.apply(null);
que se puede acortar a:
scala.Option.apply(null);
Dado este programa (OptionTest.scala
):
object OptionTest extends App {
val x = scala.None
val y = scala.Some("asdf")
}
Corrí javap
en él como esto:
javap -s -c -l -private OptionTest
Esta es una parte del javap
producción:
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
También corrí javap en scala.None
y scala.Option
. ¿Cómo se encontraría con el javap
salida que:
None
es un único objeto deNone.type
tipo que se extiendeOption
- los
apply()
Se requiere el método para el objeto complementario
?
Solución
Existen reglas de cómo se compila el código Scala a JVM-ByTecode. Debido a los posibles enfrentamientos de nombre, el código generado no siempre es intuitivo de entender, pero si las reglas se conocen, es posible obtener acceso al código SCALA compilado dentro de Java.
Atención: Mientras escribí esto, noté que Javac y Eclipse-Javac se comportan de manera diferente al acceder al código SCALA desde Java. Es posible que el código a continuación se compile con uno de ellos, pero no con el otro.
Clases, constructores, métodos
No hay reglas especiales aquí. La siguiente clase de 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
}
Se puede acceder como una clase Java normal. Se compila en un archivo llamado X.class
:
X x = new X(7);
x.m1();
x.m2(3, 5);
x.m3(3, 5);
Observe que para los métodos sin una lista de parámetros se crea una lista de parámetros vacía. Múltiples listas de parámetros se fusionan con una sola.
Campos, valores
Para una clase class X(var i: Int)
Se crean getters y setters. Para una clase class X(val i: Int)
Solo se crea 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
Observe que en Java un identificador no puede incluir signos especiales. Por lo tanto, Scalac genera para cada uno de estos signos especiales un nombre específico. Hay una clase Scala.Reflect.nametransformer que puede codificar/decodificar el OPS:
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 clase class X { var i = 5 }
se traduce por el mismo esquema que cuando el campo se crea en el constructor. Acceso directo a la variable i
De Java no es posible, porque es privado.
Objetos
No existe un objeto Scala en Java. Por lo tanto, Scalac tiene que hacer algo de magia. Por un objeto object X { val i = 5 }
Se generan dos archivos de clase JVM: X.class
y X$.class
. El primero funciona como una interfaz, incluye métodos estáticos para acceder a campos y métodos del objeto Scala. Este último es una clase singleton que no puede ser instanciada. Tiene un campo que contiene la instancia singleton de la clase, nombrada MODULE$
, que permite el acceso al singleton:
X.i();
X$.MODULE$.i();
Clases de casos
El compilador de Scala genera automáticamente un método de aplicación para una clase de casos y obteniendo para campos. La clase de casos case class X(i: Int)
se accede fácilmente:
new X(3).i();
X$.MODULE$.apply(3);
Rasgos
Un rasgo trait T { def m }
, que contiene solo miembros abstractos, se compila en una interfaz, que se coloca en los archivos de clase llamados T.class
. Por lo tanto, puede implementarse fácilmente mediante una clase Java:
class X implements T {
public void m() {
// do stuff here
}
}
Si el rasgo contiene miembros concretos, hay un archivo de clase llamado <trait_name>$class.class
Generado, adicionalmente a la interfaz normal. El rasgo
trait T {
def m1
def m2 = 5
}
También puede implementarse fácilmente dentro de Java. El archivo de clase T$class.class
Contiene los miembros concretos del rasgo, pero parece que son imposibles de acceder desde Java. Ni Javac ni el Eclipse-Javac compilarán un acceso a esta clase.
Se pueden encontrar algunos detalles más sobre cómo se compilan los rasgos aquí.
Funciones
Los literales de funciones se compilan como instancias anónimas de las clases Functionn. Un objeto 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
}
}
se compila a los archivos de clase normales, como se describe anteriormente. Además, cada función literal obtiene su propio archivo de clase. Entonces, para los valores de función un archivo de clase llamado <class_name>$$anonfun$<N>.class
se genera, donde n es un número continuo. Para métodos de función (métodos, que devuelven una función) un archivo de clase llamado <class_name>$$anonfun$<method_name>$<N>.class
es generado. Las partes del nombre de la función están separadas por signos de dólar y frente al anonfun
Identificador también hay dos signos de dólar. Para las funciones anidadas, el nombre de la función anidada se adjunta a la función exterior, esto significa que una función interna obtendrá un archivo de clase como <class_name>$$anonfun$<outer_method_name>$<N>$$anonfun$<inner_method_name>$<N>.class
. Cuando una función interna no tiene un nombre, como se ve en h
obtiene el nombre apply
.
Esto significa que en nuestro caso obtenemos:
X$$anonfun$1.class
para FX$$anonfun$g$1.class
por gX$$anonfun$h$1$$anonfun$apply$1.class
Para HX$$anonfun$i$1.class
yX$$anonfun$i$1$$anonfun$j$1$1.class
para yo y j
Para acceder a ellos, use su método de aplicación:
X.f().apply(7);
X.g().apply(7);
X.h().apply(3).apply(5);
X.i().apply(3).apply(5);
Responder a la pregunta
Usted debe saber:
- Se puede acceder a una clase SCALA normal por sus constructores o sus métodos de aplicación
- Cuando no hay constructor de lo que hay un método de aplicación
- Cuando no hay un constructor ni un método de aplicación, hay otro archivo de clase llamado de la misma manera que se llama la clase que agrega un signo de dólar al final. Buscar esta clase en busca de un
MODULE$
campo - Los constructores y los métodos de aplicación se heredan, por lo que busque las súper clases si no puede encontrar nada en las subclases
Algunos ejemplos
Opción
// 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 dice que tiene un constructor y un método de aplicación. Además, dice que la clase es abstracta. Por lo tanto, solo se puede usar el método de aplicación:
Option.apply(3);
Alguno
// javap scala.Some
public final class scala.Some extends scala.Option implements ... {
...
public scala.Some(java.lang.Object);
}
Tiene un constructor y un método de aplicación (porque sabemos que la opción tiene una y algunas opciones). Usa uno de ellos y sé feliz:
new Some<Integer>(3);
Some.apply(3);
Ninguna
// javap scala.None
public final class scala.None extends java.lang.Object{
...
}
No tiene constructor, ningún método de aplicación y no extiende la opción. Entonces, echaremos un vistazo 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í! Encontramos un MODULE$
campo y el método de aplicación de la opción. Además, encontramos el constructor privado:
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
Lista
scala.collection.immutable.List
es abstracto, por lo que tenemos que usar scala.collection.immutable.List$
. Tiene un método de aplicación que espera un scala.collection.Seq
. Entonces, para obtener una lista, primero necesitamos un SEQ. Pero si buscamos SEQ, no hay método aplicado. Además, cuando miramos las súper clases de SEQ y en scala.collection.Seq$
Solo podemos encontrar los métodos de aplicación que espera un SEQ. ¿Entonces lo que hay que hacer?
Tenemos que echar un vistazo a cómo Scalac crea una instancia de lista o SEQ. Primero cree una clase de Scala:
class X {
val xs = List(1, 2, 3)
}
Compile con Scalac y mire el archivo de clase 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
}
El constructor es interesante. Nos dice que se crea una matriz de ints (l. 12) que se llena con 1, 2 y 3. (l. 14-25). Después de eso, esta matriz se entrega a scala.Predef$.wrapIntArray
(l. 26). Esto resultante scala.collection.mutable.WrappedArray
se entrega nuevamente a nuestra lista (l. 29). Al final, la lista se almacena en el campo (l. 32). Cuando queremos crear una lista en Java, tenemos que hacer lo mismo:
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 }));
Esto se ve feo, pero funciona. Si crea una biblioteca bonita que envuelve el acceso a la biblioteca Scala, será fácil usar Scala de Java.
Resumen
Sé que hay algunas reglas más cómo se compila el código SCALA para bytecode. Pero creo que con la información anterior debería ser posible encontrar estas reglas por usted mismo.
Otros consejos
No estoy compitiendo con la otra respuesta, pero dado que las personas a menudo parecen no darse cuenta de esto, puedes hacer esto en el 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