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 (e.g.
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.