为什么 Java 中的单例模式中 x 和 y 输出不同的值?

flo*_*wer 1 java singleton java-8

在下面的代码中,为什么x和y分别输出0和1?为什么当我把这段代码\xef\xbc\x9aprivate static Singleton instance = new Singleton();从位置\xe2\x91\xa0放到位置\xe2\x91\xa1时,输出是1, 1?

\n
   private static Singleton instance = new Singleton();\n//\xe2\x91\xa0\nprivate static int x = 0;\n\nprivate static   int y;\n//\xe2\x91\xa1\nprivate Singleton()\n{\n    System.out.println("Starting add");\n    x++;\n    y++;\n}\n\npublic static Singleton getInstance()\n{\n    return instance;\n}\n\npublic static void main(String[] args)\n{\n    System.out.println("Starting Singleton");\n    Singleton singleton = Singleton.getInstance();\n    //Singleton singleton = new Singleton();\n    System.out.println(singleton.x);\n    System.out.println(singleton.y);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

rzw*_*oot 5

不要写这种代码。正如您所发现的,这非常令人困惑。然而,根据规范确实很奇怪。让我们通过相当多的步骤来解释这一点。这是关于复杂且通常不相关的细节的个别细节,这些细节结合起来解释了这种行为。

\n

CTC 和初始化器之间的区别

\n

想象一下这段代码:

\n
public class Foo {\n  private static final long MARK = System.currentTimeMillis();\n}\n
Run Code Online (Sandbox Code Playgroud)\n

显然, 的值MARK无法在编译时确定 - 这被解释为“时间戳,就像该字段初始化时一样”,归结为“加载此类时”。这意味着第一次任何地方的任何代码(如引用)都Foo必须执行某些代码。在此执行期间,上下文必须存在 - 这样的代码理论上可以引用MARK.

\n

因此,MARK 从 开始0该代码编译为:

\n
public class Foo {\n  private static final long MARK;\n\n  static {\n    // Until MARK is set, MARK is 0.\n    MARK = System.currentTimeMillis();\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

那是合法的java代码(尝试一下;编译它)——那是一个静态初始化程序。跑去javap -c -v Foo看这个东西。

\n

这:

\n
public class Foo {\n  private static final int FOO = 5;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

非常不同的。如果你运行javap -c -v Foo它,你会发现根本没有静态初始化程序。相反,5 是所谓的 CTC:编译时间常数。编译器本身会解析它,并将该值直接放入类文件中。在第一种情况下,MARKpofed 存在包含 0,然后在初始化期间设置其值。但是,立刻就突然FOO出现了价值5 。

\n

完全取决于您的代码中 CTC 的定义方式,x并且y 符合条件;具体来说x 没有恒定值,这是因为要符合条件,该字段必须是static and final。显然,两者都不是singleton。因此,您的代码与以下内容相同:

\n
 private static Singleton instance;\n//\xe2\x91\xa0\nprivate static int x;\n\nprivate static   int y;\n//\xe2\x91\xa1\n\nstatic {\n  instance = new Singleton();\n  x = 0;\n}\n\nprivate Singleton()\n{\n    System.out.println("Starting add");\n    x++;\n    y++;\n}\n\npublic static Singleton getInstance()\n{\n    return instance;\n}\n\n// ....\n}\n
Run Code Online (Sandbox Code Playgroud)\n

现在应该很明显为什么你得到 0/1 而不是预期的 1/1。首先,构造函数运行,并且xy都为零。构造函数将它们设置为 1/1。然后x被设置为0。

\n

但是..忘记这一切

\n

真正的问题是,这段代码很愚蠢。你不创建单例和静态字段,这根本没有意义。构造函数不应该充当初始化器static。构造函数从根本上来说与实例相关。因此,永远不应该编写此代码,它与自身不一致,并且您需要从前到后了解 JLS 才能了解正在发生的情况。

\n

单例的一般方法是拥有所有非静态内容,除了包含单例及其访问器的字段:

\n
class Foo {\n  private static final Foo INSTANCE = new Foo();\n  private int x = 0;\n  private int y;\n\n  private Foo() {\n    x++;\n    y++;\n  }\n\n  public static Foo getInstance() { return INSTANCE; }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

上面的效果很好。

\n

或者,如果您有需要初始化的静态内容,请编写一个实际的静态初始化程序。如果你有这个,单例通常没有意义——只需将所有内容都静态化,此时单例将毫无意义:

\n
public class Foo {\n  private static int x = 0;\n  private static int y = 0;\n\n  static {\n    x++;\n    y++;\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

也可以正常工作(这两个片段中的 x 和 y 都是 1 和 1)。

\n