为什么静态块中的Java静态变量为null,取决于访问顺序

Why is Java static variable null in the static block, depending on the access order

以下 java 代码给出如下输出。注意 B.list 里面有 null

A
[null]
import java.util.*;

public class Main{
     public static void main(String []args){
        System.out.println(A.VAR_A);
        System.out.println(B.list);
     }
}

class A {
  public static final String VAR_A = B.id("A");
}

class B {
  public static final List<String> list = new ArrayList<>();
  static {
    list.add(A.VAR_A);
  }

  public static String id(String s) {
    return s;
  }
}

但是,如果您像下面这样交换 println:

public class Main{
     public static void main(String []args){
        System.out.println(B.list);
        System.out.println(A.VAR_A);
     }
}

列表已正确初始化:

[A]
A

为什么 B.list 在第一个代码中有 null 而不是 "A"?

您可以在此处在线测试代码:https://onlinegdb.com/SkVbLQwm_

更有趣的是,下面的代码给出了 B.list = [null, "B"]

A                                                                                                              
[null, B]
import java.util.*;

public class Main
{
  public static void main (String[]args)
  {
    System.out.println (A.VAR_A);
    System.out.println (B.list);
  }
}

class A
{
  public static final String VAR_A = B.id ("A");
  public static final String VAR_B = "B";
}

class B
{
  public static final List < String > list = new ArrayList <> ();
  static
  {
    list.add (A.VAR_A);
    list.add (A.VAR_B);
  }

  public static String id (String s)
  {
    return s;
  }
}

System.out.println(A.VAR_A);

这是(大概)VM 中的任何代码 运行ning 第一次提到 A class。因此,该语句首先以 'loading' A class 结束。 (Java 根据需要加载 classes;它不会预先加载它们)。

要加载 A class,必须首先执行所有静态初始化程序。静态初始化器是所有不是编译时常量的静态变量的初始化表达式,以及 static {} 块中的所有代码,按顺序执行。例如,假设:

class Example {
    public static final long loadedAt = System.currentTimeMillis();
}

因为System.cTM不是常量,那是需要执行的代码;此代码由系统在加载 A 时执行。

在您粘贴的代码片段中,A 的静态初始化代码是:

 VAR_A = B.id("A");

并且在执行此代码期间...B 被加载,因为这是 B 第一次被提及。这也涉及运行B的静态初始化代码。所以在执行A的初始化程序的过程中,B的初始化程序被执行。让我们看看代码是什么样的:

list = new ArrayList<>();
list.add(A.VAR_A);

A 尚未加载(请记住,我们已经完成了一半。还没有完全完成)。因此,作为 B 的初始化器的一部分,我们必须 运行 A 的初始化器。

哪个 运行 B 的初始化器,哪个 运行 A 的初始化器,因此你写了一个无限循环。

为了避免这种情况,java class加载系统有一个特殊的怪异规则:

  • 每当 class 的初始化开始时,VM 都会将 class 添加到一个特殊的 'in the process of being initialized' 列表中。
  • 每当系统被要求初始化一个 class 时,VM 首先检查 class 是否在 'we are in the middle of initializing it' 列表中。如果是,那么什么也不会发生,class,处于(可能损坏!)半载状态,按原样提供。

因此,在 A 的初始化期间,B 被初始化,并且在 B 的初始化期间,A 处于半加载状态。因此,A.VAR_A 未设置 YET,因此是 null。这正是您所看到的。

换个说法,场景就变了:现在启动了B,过程进行到一半,又启动了A,由于特殊规则,A以半生不熟的状态提供给A。

第二个片段引入了一个新概念:编译时间常数。

字符串和基元可以是 CTC。这些 not 编译为静态初始值设定项。这个例子 class 根本没有初始化器:

class Example {
    public static final String HELLO = "Hello";
}

那是因为这是一个 CTC。 CTC 的规则是:

  • 变量是静态的和最终的。
  • 它在声明时被赋予了一个值。
  • 分配的值是 'CTC expression',定义为:字符串文字、数字文字、字符文字或对另一个 CTC 表达式的引用(例如 public static final String HELLO = SomeOtherClass.SOME_OTHER_CONSTANT;), 或左右为CTC表达式的简单数学运算

"B" 因此是一个 CTC 表达式。因此,编译器将在编译时解析表达式并将结果值直接存储到 class 文件中,而不是作为代码,而是作为它自己的值。

因此,A.VAR_B 就像 'search replace' - 它不需要初始化 B; VAR_B 字段是常量,突然出现并具有值“B”,而 VAR_A 突然出现为 null,并被分配通过执行表达式 [=] 获得的值21=] 在初始化期间。

你可以看到这些东西在起作用。 运行 javap -c -v YourType 你将能够观察到这一切。玩弄你的代码片段是个好主意,运行在它们上面 javap,看看 CTC 和初始化程序之间的区别。