Scala 中的单例对象(Singleton Objects)是如何实现的
结论
Scala 的单例对象是通过“饿汉模式”(即Eager Initialization)来实现的。具体说来,是在对应的类初始化时,单例对象被赋值。
代码
Singleton Objects 一文提到
An object is a class that has exactly one instance
那么 Scala 中的单例对象是如何实现的呢?
我们用以下代码来进行探索。(请将代码保存成 Main.scala)
object AreaCalculator {
def PI = Math.PI;
def calculateArea(r: Double): Double = {
PI * r * r
}
}
class Main {
def f() = {
AreaCalculator.calculateArea(1.0)
}
}
单例对象 AreaCalculator 中定义了 calculateArea 方法,用于计算圆的面积。
如下命令可以编译 Main.scala。(我使用的 scalac 的版本是 3.6.2,执行 scalac -version 可以看到对应的版本)
scalac Main.scala
编译后,会生成以下 class 文件。
AreaCalculator.class
AreaCalculator$.class
Main.class
查看 Main 类
Main 类的逻辑应该比较简单,我们先看它。
如下命令可以查看 Main.class 的内容。
javap -v -p Main
部分结果如下(常量池等部分略去)
{
public Main();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #9 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LMain;
public double f();
descriptor: ()D
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #19 // Field AreaCalculator$.MODULE$:LAreaCalculator$;
3: dconst_1
4: invokevirtual #23 // Method AreaCalculator$.calculateArea:(D)D
7: dreturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this LMain;
}
假如源码是 java 的话,那么可以认为它应该是这样的 ⬇️
// 以下代码是我手动转化的,不保证绝对准确,仅供参考。
public class Main {
public Main() {
super();
}
public double f() {
AreaCalculator$.MODULE$.calculateArea(1.0d);
}
}
看来 f 方法中,通过 AreaCalculator$ 类的 MODULE$ 字段调用了 calculateArea(double) 方法。
但 AreaCalculator$ 类以及它的 MODULE$ 字段具体是什么样子呢?
查看 AreaCalculator$ 类
我们接着去查看 AreaCalculator$ 类。
如下命令可以查看 AreaCalculator$.class 的内容。
javap -v -p 'AreaCalculator$'
部分结果如下(常量池等部分略去)
{
public static final AreaCalculator$ MODULE$;
descriptor: LAreaCalculator$;
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
private AreaCalculator$();
descriptor: ()V
flags: (0x0002) ACC_PRIVATE
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #13 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LAreaCalculator$;
public static {};
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #2 // class AreaCalculator$
3: dup
4: invokespecial #16 // Method "<init>":()V
7: putstatic #18 // Field MODULE$:LAreaCalculator$;
10: return
LineNumberTable:
line 2: 0
private java.lang.Object writeReplace();
descriptor: ()Ljava/lang/Object;
flags: (0x0002) ACC_PRIVATE
Code:
stack=3, locals=1, args_size=1
0: new #22 // class scala/runtime/ModuleSerializationProxy
3: dup
4: ldc #2 // class AreaCalculator$
6: invokespecial #25 // Method scala/runtime/ModuleSerializationProxy."<init>":(Ljava/lang/Class;)V
9: areturn
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this LAreaCalculator$;
public double PI();
descriptor: ()D
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: ldc2_w #28 // double 3.141592653589793d
3: dreturn
LineNumberTable:
line 2: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this LAreaCalculator$;
Signature: #27 // ()D
public double calculateArea(double);
descriptor: (D)D
flags: (0x0001) ACC_PUBLIC
Code:
stack=4, locals=3, args_size=2
0: aload_0
1: invokevirtual #34 // Method PI:()D
4: dload_1
5: dmul
6: dload_1
7: dmul
8: dreturn
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LAreaCalculator$;
0 9 1 r D
MethodParameters:
Name Flags
r final
}
假如源码是 java 的话,那么可以认为它应该是这样的 ⬇️
// 以下代码是我手动转化的,不保证绝对准确,仅供参考。
public final class AreaCalculator$ implements java.io.Serializable {
// MODULE$ 字段是 AreaCalculator$ 类的唯一实例
public static final AreaCalculator$ MODULE$;
// 构造函数
private AreaCalculator$() {
super();
}
// 在 AreaCalculator$ 类初始化的时候,对唯一的实例 MODULE$ 进行赋值,所以是“饿汉模式”
static {
MODULE$ = new AreaCalculator$();
}
// 我也不知道这个方法 ⬇️ 是做什么用的。我只是按照它的 Code 属性,把对应的 java 代码给转化出来了
private java.lang.Object writeReplace() {
return new scala.runtime.ModuleSerializationProxy(AreaCalculator$.class);
}
public double PI() {
return 3.141592653589793d;
}
public double calculateArea(double r) {
return PI() * r * r;
}
}
从上面的代码可以看出 AreaCalculator$ 类中的 MODULE$ 字段是这个类的唯一实例,而这个实例是在 static 语句块中被赋值的。也就是说在 AreaCalculator$ 类初始化的时候,这个单例会被赋值,那么这就是“饿汉模式”了。
查看 AreaCalculator 类
现在只剩下 AreaCalculator 类了。
如下命令可以查看 AreaCalculator.class 的内容。
javap -v -p 'AreaCalculator'
部分结果如下(常量池等部分略去)
{
public static double PI();
descriptor: ()D
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #13 // Field AreaCalculator$.MODULE$:LAreaCalculator$;
3: invokevirtual #15 // Method AreaCalculator$.PI:()D
6: dreturn
Signature: #7 // ()D
public static double calculateArea(double);
descriptor: (D)D
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: getstatic #13 // Field AreaCalculator$.MODULE$:LAreaCalculator$;
3: dload_0
4: invokevirtual #19 // Method AreaCalculator$.calculateArea:(D)D
7: dreturn
}
假如源码是 java 的话,那么可以认为它应该是这样的 ⬇️
// 以下代码是我手动转化的,不保证绝对准确,仅供参考。
public final class AreaCalculator {
public static double PI() {
return AreaCalculator$.MODULE$.PI();
}
// 获取 AreaCalculator$ 类的单例对象后,再调用它的 calculateArea(double) 方法
public static double calculateArea(double var0) {
return AreaCalculator$.MODULE$.calculateArea(var0);
}
}
用 Java 代码来验证
我们可以用如下的 java 代码来进行验证。(请将代码保存为 SimpleTest.java)
public class SimpleTest {
public static void main(String[] args) {
double r = 1.0d;
double area = AreaCalculator.calculateArea(r);
System.out.println("Area is: " + area);
}
}
如下命令可以编译 SimpleTest.java 并运行其中的 main 方法。
javac SimpleTest.java && java SimpleTest
运行结果如下
整个调用过程可以分为以下步骤。
SimpleTest类的main方法调用AreaCalculator类的 静态方法calculateArea(double)- 在
AreaCalculator类的 静态方法calculateArea(double)中- 获取
AreaCalculator$的单例对象(在MODULE$字段中), - 通过这个单例对象来调用
AreaCalculator$类中的calculateArea(double)方法(这是一个 非静态方法)
- 获取
graph TD
node1["SimpleTest 类调用 AreaCalculator 类中的静态方法 calculateArea(double)"] --> node2["AreaCalculator 类获取 AreaCalculator$ 的单例对象 (在 MODULE$ 字段中)"] --> node3["通过单例对象 MODULE$ 调用 calculateArea(double) 方法"]