Scala 伴生对象的细节

在Scala中,想要通过主类运行程序,一般我们并没有声明main方法,而是通过单例对象继承App来实现的。如果单例对象和同名特质同时出现在同一作用域则会报如下警告: A的伴生”类”是一个特质,无法生成静态代理,所以导致单例对象A无法成为一个可运行的程序。

scala> trait A
defined trait A
scala> object A extends App
defined object A
warning: previously defined trait A is not a companion to object A.
Companions must be defined together; you may wish to use :paste mode for this.

在解答这个问题之前,先来了解一下什么是静态代理,以单例对象为例来展开。

object A { def foo = 1 }

编译之后会生成两个类A.class, A$.class

$ javap A
警告: 二进制文件A包含me.ooon.A
Compiled from "A.scala"
public final class me.ooon.A {
public static int foo();
}

$ javap A\$
警告: 二进制文件A$包含me.ooon.A$
Compiled from "A.scala"
public final class me.ooon.A$ {
public static final me.ooon.A$ MODULE$;
public static {};
public int foo();
}

在《Programming In Scala》一书中对于单例对象有这样一段解释:

For a singleton object named App, the compiler produces a Java class named App\$ .This class has all the methods and fields of the Scala singleton object. The Java class also has a single static field named MODULE$ to hold the one instance of the class that is created at run time.

对于单例对象A,编译器会生成一个Java类A\$,它包含单例对象中所有的方法和属性,同时还有一个静态的属性MODULE\$引用了一个运行时生成的A\$实例。具体到上面的代码A\$为编译器生成的Java类包含了单例对象中的foo方法和静态属性MODULE\$。

如果只有这个类的话,那么我们在Java中我们就需要这样调用A\$.MODULE\$.foo(), 它要求我们了解A\$内部结构并且也不友好。 所以A出现了, 这样在Java中可以 通过A.foo()来调用(A$中的方法),A.class中的静态方法实际上就是前面提到的静态代理。

public class MainTest {
public static void main(String[] args) {
int r1 = A$.MODULE$.foo();
int r2 = A.foo();
System.out.println(r1 == r2); // true
}
}

从下面的反编译信息可以看出 A.foo(), 最终是A\$.MODULE\$.foo()

$ javap -c A
警告: 二进制文件A包含me.ooon.A
Compiled from "A.scala"
public final class me.ooon.A {
public static int foo();
Code:
0: getstatic #16 // Field me/ooon/A$.MODULE$:Lme/ooon/A$;
3: invokevirtual #18 // Method me/ooon/A$.foo:()I
6: ireturn
}

当同一作用域内同时出现了伴生类A和伴生对象A时,编译时除了在A.class中生成静态代理之外,还会添加伴生类本来的方法。

object A { def foo = 1 }
class A

$ scalac -Ylog:jvm A.scala
[log jvm] missingHook(package <root>, android): <none>
[log jvm] Adding static forwarders from 'class A' to implementations in 'object A'
[log jvm] No mirror class for module with linked class: me.ooon.A

回到开始的案例,当同一作用域内同时出现了特质和单例对象时,编译器会尝试在A.class中添加静态代理,但此时trait A编译之后实际上是Java中的接口,而接口是不能定义静态方法的。 所以A.class中并没有对应的main方法,所以不能成为一个可运行的程序。

stackoverflow 上的讨论:Singletons as Synthetic classes in Scala?