Scala 代码如何编译为 JVM 字节码是有规则的。由于潜在的名称冲突,生成的代码并不总是易于理解,但如果规则已知,则可以在 Java 中访问已编译的 Scala 代码。
注意:在编写本文时,我注意到 javac 和 eclipse-javac 在从 Java 访问 Scala 代码时表现不同。下面的代码可能会与其中一个一起编译,但不能与另一个一起编译。
类、构造函数、方法
这里没有特别的规则。以下 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
}
可以像普通的 Java 类一样访问。它被编译成一个名为的文件X.class
:
X x = new X(7);
x.m1();
x.m2(3, 5);
x.m3(3, 5);
请注意,对于没有参数列表的方法,会创建一个空的参数列表。多个参数列表合并为一个。
字段、值
为一个类class X(var i: Int)
创建了 Getters 和 Setters。对于一个类class X(val i: Int)
,只创建一个 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
请注意,在 Java 中,标识符不允许包含特殊符号。因此,scalac 为这些特殊符号中的每一个生成一个特定的名称。有一个类scala.reflect.NameTransformer可以对操作进行编码/解码:
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)
类class X { var i = 5 }
通过与在构造函数中创建字段时相同的模式进行转换。从 Java直接访问变量i
是不可能的,因为它是私有的。
对象
Java 中没有 Scala 对象之类的东西。因此 scalac 必须做一些魔术。对于一个对象object X { val i = 5 }
,会生成两个 JVM 类文件:X.class
和X$.class
. 第一个像接口一样工作,它包括访问 Scala 对象的字段和方法的静态方法。后者是一个无法实例化的单例类。它有一个 Field 保存类的单例实例,名为MODULE$
,它允许访问单例:
X.i();
X$.MODULE$.i();
案例类
Scala 编译器自动为案例类生成应用方法,为字段生成 Getter。案例类case class X(i: Int)
很容易访问:
new X(3).i();
X$.MODULE$.apply(3);
性状
仅包含抽象成员的 traittrait T { def m }
被编译为一个接口,该接口被放置在名为 的类文件中T.class
。因此它可以通过 Java 类轻松实现:
class X implements T {
public void m() {
// do stuff here
}
}
<trait_name>$class.class
如果 trait 包含具体成员,那么除了普通接口之外,还有一个名为 generate 的类文件。特质
trait T {
def m1
def m2 = 5
}
也可以很容易地在 Java 中实现。类文件T$class.class
包含特征的具体成员,但似乎无法从 Java 访问它们。javac 和 eclipse-javac 都不会编译对此类的访问。
可以在此处找到有关如何编译特征的更多详细信息。
职能
函数字面量被编译为类 FunctionN 的匿名实例。一个 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
}
}
被编译成普通的类文件,如上所述。此外,每个函数文字都有自己的类文件。因此,对于函数值,<class_name>$$anonfun$<N>.class
会生成一个名为的类文件,其中 N 是一个连续数。对于函数方法(返回函数的方法),<class_name>$$anonfun$<method_name>$<N>.class
会生成一个名为的类文件。函数名的各个部分用美元符号分隔,在anonfun
标识符前面还有两个美元符号。对于嵌套函数,嵌套函数的名称附加到外部函数,这意味着内部函数将获得一个类文件,如<class_name>$$anonfun$<outer_method_name>$<N>$$anonfun$<inner_method_name>$<N>.class
. 当内部函数没有名称时,如中所示,h
它会获得名称apply
。
这意味着在我们的例子中我们得到:
X$$anonfun$1.class
对于 f
X$$anonfun$g$1.class
为克
X$$anonfun$h$1$$anonfun$apply$1.class
为 h
X$$anonfun$i$1.class
对于 i和X$$anonfun$i$1$$anonfun$j$1$1.class
j
要访问它们,请使用它们的应用方法:
X.f().apply(7);
X.g().apply(7);
X.h().apply(3).apply(5);
X.i().apply(3).apply(5);
回答问题
你应该知道:
- 一个普通的 Scala 类可以被它们的构造函数或它们的应用方法访问
- 当没有构造函数比有应用方法时
- 当没有构造函数和应用方法时,还有另一个类文件,其名称与调用该类的方式相同,该类文件在末尾附加一个美元符号。在此类中搜索
MODULE$
字段
- 构造函数和应用方法是继承的,所以如果在子类中找不到任何东西,请搜索超类
一些例子
选项
// 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 说它有一个构造函数和一个 apply 方法。此外,它说该类是抽象的。因此只能使用 apply 方法:
Option.apply(3);
一些
// javap scala.Some
public final class scala.Some extends scala.Option implements ... {
...
public scala.Some(java.lang.Object);
}
它有一个构造函数和一个应用方法(因为我们知道 Option 有一个并且 Some extends Option)。使用其中之一并感到高兴:
new Some<Integer>(3);
Some.apply(3);
没有任何
// javap scala.None
public final class scala.None extends java.lang.Object{
...
}
它没有构造函数,没有应用方法,也没有扩展选项。因此,我们将看看None$
:
// javap -private scala.None$
public final class scala.None$ extends scala.Option implements ... {
...
public static final scala.None$ MODULE$;
private scala.None$();
}
是的!我们找到了一个MODULE$
字段和Option的apply-method。此外,我们找到了私有构造函数:
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
列表
scala.collection.immutable.List
是抽象的,因此我们必须使用scala.collection.immutable.List$
. 它有一个应用方法,它需要一个scala.collection.Seq
. 所以要得到一个列表,我们首先需要一个 Seq。但是如果我们看一下 Seq,就没有 apply-method。此外,当我们查看 Seq 的超类时,scala.collection.Seq$
我们只能找到一个期望 Seq 的应用方法。那么该怎么办?
我们必须看看 scalac 如何创建 List 或 Seq 的实例。首先创建一个 Scala 类:
class X {
val xs = List(1, 2, 3)
}
用scalac编译,用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
}
构造函数很有趣。它告诉我们,创建了一个整数数组(l. 12),其中填充了 1、2 和 3。(l. 14-25)。之后这个数组被传送到scala.Predef$.wrapIntArray
(l. 26)。这个结果scala.collection.mutable.WrappedArray
再次被传递到我们的列表(l. 29)。最后,列表存储在字段中(l. 32)。当我们想在 Java 中创建一个 List 时,我们必须这样做:
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 }));
这看起来很难看,但它确实有效。如果您创建一个漂亮的库来包装对 Scala 库的访问,那么从 Java 中使用 Scala 将很容易。
概括
我知道 Scala 代码如何编译为字节码还有一些规则。但是我认为有了上面的信息,你应该可以自己找到这些规则。