[Scala] 单例对象(Singleton Objects)是如何实现的

77 阅读2分钟

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

运行结果如下

image.png

整个调用过程可以分为以下步骤。

  1. SimpleTest 类的 main 方法调用 AreaCalculator 类的 静态方法 calculateArea(double)
  2. AreaCalculator 类的 静态方法 calculateArea(double)
    • 获取 AreaCalculator$ 的单例对象(在 MODULE$ 字段中),
    • 通过这个单例对象来调用 AreaCalculator$ 类中的 calculateArea(double) 方法(这是一个 非静态方法
graph TD
node1["SimpleTest 类调用 AreaCalculator 类中的静态方法 calculateArea(double)"] --> node2["AreaCalculator 类获取 AreaCalculator$ 的单例对象 (在 MODULE$ 字段中)"] --> node3["通过单例对象 MODULE$ 调用 calculateArea(double) 方法"]