简体   繁体   中英

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

The following java code gives output as below. Notice B.list has null in it.

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;
  }
}

However, if you swap the println like below:

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

The list is initialized correctly:

[A]
A

Why does B.list have null in it instead of "A" in the first code?

You can test out the code online here: https://onlinegdb.com/SkVbLQwm _

More interestingly, the code below gives 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);

This is (presumably) the first time any code running in the VM ever even mentioned the A class. Thus, this statement ends up 'loading' the A class first. (Java loads classes as needed; it does not load them all up front).

To load the A class, all static initializers must be executed first. Static initializers are all initializing expressions of static variables that aren't compile time constants, and all code in static {} blocks, executed in order. For example, imagine:

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

Because System.cTM is not a constant, that is code that needs to be executed; this code is executed by the system when A is loaded.

In the snippet you pasted, the static initializer code for A is:

 VAR_A = B.id("A");

and during the execution of this code... B gets loaded , as that is the first time B is ever mentioned. This too involves running the static initializer code of B. So in the middle of the process of executing A's initializers, B's initializers are executed . Let's see what that code looks like:

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

A is not loaded yet (remember, we were halfway done. Which is not fully done). So, as part of B's initializers, we must run A's initializers.

Which would run B's initializers, which would run A's initializers, and thus you have written an endless loop.

To avoid this situation, the java class loading system has a special weird rule:

  • Whenever initialization of a class begins, the VM adds that class to a special 'in the process of being initialized' list.
  • Whenever the system is asked to initialize a class, the VM first checks if that class is on the 'we are in the middle of initializing it' list. If it is, then nothing happens , and the class, in a (probably broken,), half-loaded state. is provided as is.

So, during A's inits, B is inited, and during B's init, A is provided in its half-loaded state. As a consequence, A.VAR_A is not set YET , and thus is null . Which is exactly what you are witnessing.

Switch the statements around, and the scenario changes: Now B is inited, halfway through that process, A is inited, and A is provided with B in a half-baked state due to the special rule.

The second snippet introduces a new concept: Compile time constants.

Strings and primitives can be CTCs. These are not compiled down as static initializers whatsoever. This example class has no initializer, at all :

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

That's because this is a CTC. The rules for CTCs are:

  • The variable is static and final.
  • It is assigned a value as it is declared.
  • That value that is assigned is a 'CTC expression', which is defined as: either a string literal, or a numeric literal, or a character literal, or a reference to another CTC expression (eg public static final String HELLO = SomeOtherClass.SOME_OTHER_CONSTANT; ), or a simple mathematical operation whose left and right hand side are CTC expressions.

"B" is therefore a CTC expression. Therefore, the compiler will, at compile time, resolve the expression and store the resulting value directly into the class file, not as code, but as its own value.

Hence, A.VAR_B is like a 'search replace' - it doesn't require that B is initialized; The VAR_B field, being constant, springs into existence already having the value "B", whereas VAR_A springs into existence as null , and gets assigned the value obtained by executing the expression B.id("A") during init.

You can see this stuff in action. Run javap -c -v YourType and you'll be able to observe all this. It's a great idea to toy around with your snippets, running javap on them, see the difference between CTCs and initializers.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM