When a class is actually initialised
Constants inline at compile time, and a static cycle yields a null you can observe.
Open this lesson in the learning hubKey points
- A class is initialised on first active use: a new instance, a static method call, or a non-constant static field read. Merely referencing the type does not trigger it.
- A
static finalprimitive or String initialised with a constant expression is a compile-time constant. The value is inlined into the caller bytecode, so reading it never initialises the class at all. - That has a real consequence for deployment: changing such a constant requires recompiling every class that reads it, not just the one that declares it.
- Static initialisers run in textual order, so a static field read before its own declaration sees the default value - zero or null - rather than the assigned one.
- A circular static dependency between two classes produces an observable null. Neither class is wrong on its own; the cycle decides which one loses, and the outcome depends on which is touched first.
- Initialisation is thread-safe and happens once, which is what makes the holder idiom the cleanest lazy singleton - the JVM does the locking for you.
Example
public class InitOrder {
static class Config {
// Compile-time constant: INLINED into callers. Reading it does
// NOT initialise Config, so the static block below never runs.
static final int MAX = 100;
// NOT a constant - the value is not known at compile time.
static final Integer BOXED = 100;
static { System.out.println(" [Config initialised]"); }
}
static class Ordering {
static int a = compute("a", 1);
static int b;
static {
// b is read BEFORE its declaration below -> still the default 0.
System.out.println(" in static block, b = " + b);
b = 2;
}
static int c = compute("c", 3);
static int compute(String name, int v) {
System.out.println(" computing " + name);
return v;
}
}
// A circular static dependency. One side WILL see null.
static class A { static final String NAME = "A"; static final String FROM_B = B.NAME; }
static class B { static final String NAME = "B"; static final String FROM_A = A.NAME; }
// The holder idiom: lazy, thread-safe, no synchronisation written by you.
static class Heavy {
private Heavy() { System.out.println(" [Heavy constructed]"); }
private static class Holder { static final Heavy INSTANCE = new Heavy(); }
static Heavy get() { return Holder.INSTANCE; }
}
public static void main(String[] args) {
System.out.println("read Config.MAX (compile-time constant):");
System.out.println(" MAX = " + Config.MAX + " <- no init message above");
System.out.println("read Config.BOXED (not a constant):");
System.out.println(" BOXED = " + Config.BOXED);
System.out.println("static initialiser order:");
System.out.println(" a=" + Ordering.a + " b=" + Ordering.b + " c=" + Ordering.c);
System.out.println("circular static dependency:");
System.out.println(" A.FROM_B = " + A.FROM_B);
System.out.println(" B.FROM_A = " + B.FROM_A + " <- one side loses");
System.out.println("holder idiom (constructed only now):");
Heavy.get();
}
}
A static final constant is inlined into callers, and a static cycle leaves one side observing null - order of first touch decides which.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Core Java course, and every lesson in it is listed on the Core Java contents page.