0

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;
  }
}
czheo
  • 1,771
  • 2
  • 12
  • 22
  • 1
    The static initializers get run in the order in which their classes are initialized .. see . – Mr R Mar 11 '21 at 05:23
  • @MrR that doesn't explain the last example where B.list = [null, "B"]. Initialization of VAR_B is executed but not for VAR_A, though VAR_A is defined before VAR_B in the same class. – czheo Mar 11 '21 at 05:39
  • The long answer explains that case - while A is initializing it tried to make a Call on B, and while B initialize it then tries to use the partially initialized `VAR_A` - and get's null. They can't both complete initialization without a tie breaker rule like this .. – Mr R Mar 11 '21 at 06:47

1 Answers1

4

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.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • 1
    *"To load the A class, all static initializers must be executed first."* - No. Loading and initializing are [two different processes](https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-5.html). Static initializers are [not executed](https://docs.oracle.com/javase/specs/jls/se15/html/jls-12.html#jls-12.4.1) during class loading. Of course, there are [ways](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Class.html#forName(java.lang.String,boolean,java.lang.ClassLoader)) to load a class without invoking static initializer. – apangin Mar 11 '21 at 08:27
  • @apangin pedantic details. If you write `A.VAR_A`, A's static initializers are going to be executed first. It's an SO answer, not a complete restating of the entire JVMS. – rzwitserloot Mar 11 '21 at 08:28
  • 1
    IMO, it's OK for SO answers to be simplified, but not to distort the facts. – apangin Mar 11 '21 at 08:31
  • 3
    It’s not pedantic when a sentence is illogical. You can’t initialize a class before it has been loaded, so the sentence “*To load the A class, all static initializers must be executed first.*” makes no sense. Besides that, the correct answer would be even simpler. Just go through every occurrence of “load” and either, replace it with “initialize” when it actually means that or remove it completely when makes false statements about class loading. The JVM may load the classes upfront, but the behavior wouldn’t change. – Holger Mar 11 '21 at 09:45
  • You're ascribing meanings to the word 'load' that aren't universally accepted. – rzwitserloot Mar 11 '21 at 19:55