Java · Java Essentials

Java: Class Initialization and Singletons

When a class initializes and in what order, what a failed static initializer leaves behind, why that makes eager, holder and enum singletons thread-safe - and how reflection, serialization and classloaders make extra instances.

  • 7 min read
  • 14 practice questions
  • 1 graded problem
On this page
  1. Description
  2. When a class gets set up
  3. The order things run in
  4. When a static initializer throws
  5. Singletons
  6. How a singleton gets a twin
  7. Common mistakes
  8. Check yourself
  9. Key takeaways
  10. Quick revise
  11. Practice

Description

When does the code in a static block actually run? In what order do constructors and field initializers fire when one class extends another? And how do you make sure an object is created exactly once, even when many threads ask for it at the same time? These questions sound academic, but they explain real behavior: why one way of writing a singleton is safe and another isn't, why a startup error can break a class until the server restarts, and what a puzzling "what does this print?" interview question is really testing.

The good news is that the JVM follows a few simple, strict rules. Once you know them, the rest follows.

Here's the path, from easiest to most technical:

  1. When a class gets set up (initialized), and the one big exception: constants.
  2. The order things run in when you create an object.
  3. What happens when a static initializer throws.
  4. Four ways to write a singleton, and when each one is right.
  5. How a "single" instance can end up with a twin.

When a class gets set up

Java doesn't prepare every class when your program starts. A class is initialized (its static fields get their values and its static {} blocks run, top to bottom) the first time it's really used:

  • creating an object with new,
  • calling one of its static methods,
  • reading one of its static fields (with one exception, below).

A subclass also initializes its parent class first.

The exception is a compile-time constant: a static final field of a primitive type or String, set to a value the compiler can work out on its own. The compiler copies the value straight into the code that uses it, so reading it never touches the class:

class Metrics {
    static final String VERSION = "1.4";                               // a constant: copied into callers
    static final String BUILT_AT = String.valueOf(System.nanoTime());  // computed when the program runs
    static { System.out.println("Metrics initialized"); }
}

Metrics.VERSION     // prints nothing: the class isn't initialized
Metrics.BUILT_AT    // prints "Metrics initialized" first

Two promises come with initialization, and the rest of this lesson builds on them:

  • It happens exactly once per class.
  • It happens under a lock. If another thread needs the class while it's being set up, that thread waits, and then sees all the static fields fully set.

The order things run in

Creating an object runs code from several places. Here's a parent and a child class that announce each step:

class Base {
    static { System.out.println("Base static"); }
    { System.out.println("Base instance"); }           // an instance initializer block
    Base() { System.out.println("Base constructor"); }
}
class Child extends Base {
    static { System.out.println("Child static"); }
    { System.out.println("Child instance"); }
    Child() { System.out.println("Child constructor"); }
}

new Child();   // Base static, Child static, Base instance, Base constructor, Child instance, Child constructor
new Child();   // Base instance, Base constructor, Child instance, Child constructor

Two rules explain it:

  1. Static setup happens once per class, parent first. That's why the second new Child() skips it.
  2. Every new object is built from the top of the family down. For each class, starting with the parent, Java runs its field initializers and instance blocks, then its constructor body.

Rule 2 is why a parent constructor that calls an overridable method is dangerous: the child's version runs before the child's own fields have been set.

When a static initializer throws

class Config {
    static final int PORT = Integer.parseInt(System.getProperty("port", "eighty"));
}

"eighty" isn't a number, so the first use of Config throws an ExceptionInInitializerError that wraps the real NumberFormatException. Here's the part that surprises people: Java marks the class as broken and never tries again. Every later use throws NoClassDefFoundError: Could not initialize class Config, even after someone fixes the setting, until the JVM restarts.

The lesson: keep static initializers simple. Load configuration in a normal method that you can call, retry and test.

Singletons

A singleton is a class that should have exactly one instance, such as a shared cache or a connection registry. There are four common ways to write one in Java.

1. Eager. Create the instance when the class initializes. Thread-safe for free, thanks to the class-initialization lock:

class Eager {
    private static final Eager INSTANCE = new Eager();
    private Eager() {}
    static Eager getInstance() { return INSTANCE; }
}

The catch: calling any static method on Eager creates the instance, even one that doesn't need it.

2. The holder idiom. Lazy (created only when first asked for) and still lock-free:

class Lazy {
    private Lazy() {}
    private static class Holder { static final Lazy INSTANCE = new Lazy(); }
    static Lazy getInstance() { return Holder.INSTANCE; }
}

Holder isn't initialized until getInstance() first touches it, and from then on getInstance() is a plain field read.

3. An enum. The JVM creates each enum constant exactly once, and it also blocks the two tricks in the next section:

enum Registry { INSTANCE; }

4. Double-checked locking. Lazy, and it locks only while creating the instance:

class Checked {
    private static volatile Checked instance;      // volatile is required
    private Checked() {}
    static Checked getInstance() {
        Checked local = instance;
        if (local == null) {                       // first check, without the lock
            synchronized (Checked.class) {
                local = instance;
                if (local == null) instance = local = new Checked();   // second check, with it
            }
        }
        return local;
    }
}

Without volatile, another thread can see the reference before the constructor's work is visible, and use a half-built object. (The Java Multithreading module explains why volatile fixes this.) A simpler alternative is to make the whole getInstance() method synchronized: correct, but it takes a lock on every call.

Which one should you pick?

You needUse
Simplest, and it's cheap to createeager
Lazy, and creating it can't failholder idiom
Strongest protection against extra instancesenum
Lazy, and creating it might fail and need a retrydouble-checked locking (or a synchronized method)

The last row matters because of the previous section: if a holder's initializer throws, the holder class is broken until restart, so a failed first attempt can never be retried.

How a singleton gets a twin

A private constructor keeps honest code out, but three things can still create a second instance of a class-based singleton:

  • Reflection can call a private constructor: constructor.setAccessible(true); constructor.newInstance().
  • Deserialization creates a new object without calling any constructor. A readResolve() method returning INSTANCE fixes it.
  • Separate class loaders. A class is unique per class loader, so two web applications in one server that each bundle the same library each get their own copy of the class, and of its "single" instance. Enums don't help here.

An enum handles the first two automatically. In everyday application code, most "singletons" are simply one object that a dependency-injection framework (like Spring) creates and shares; these idioms matter for libraries, for code without a framework, and for interviews.

Common mistakes

MistakeWhat happensDo this instead
Heavy work or I/O in a static initializerone failure breaks the class until restartdo it in a method you can retry
Expecting a constant to trigger initializationnothing runs: the value was copied inuse a method, or a non-constant field
An eager singleton with unrelated static methodsthe instance is built too earlyholder idiom or enum
Double-checked locking without volatileanother thread can see a half-built objectadd volatile
A holder whose constructor can failthe failure is permanentdouble-checked locking, or synchronized
Trusting a private constructorreflection and deserialization make twinsenum, or readResolve
A parent constructor calling an overridable methodthe child's code runs before its fields are setcall only private/final methods

Check yourself

Try to answer before opening each one. Recalling beats re-reading.

1. Why is the holder idiom lazy and thread-safe without any lock in getInstance()?

Show answer

Holder initializes only on its first use, which happens inside getInstance(), and the JVM runs a class's initialization exactly once, under a lock. Every thread sees the one finished instance; after that, getInstance() is a plain field read.

2. A static initializer threw on first use. Why does every later call fail with NoClassDefFoundError?

Show answer

The JVM marks a class whose initialization failed as unusable and never retries it. The first use gets ExceptionInInitializerError; every later use gets NoClassDefFoundError, until the JVM restarts.

3. What order do static blocks, instance blocks and constructors run in for the first new Child()?

Show answer

Base's static setup, then Child's; then Base's instance blocks and constructor; then Child's instance blocks and constructor.

4. Reading Metrics.VERSION (a static final String set to "1.4") doesn't run Metrics's static block. Why?

Show answer

It's a compile-time constant, so the compiler copied "1.4" into the calling code. The program never touches the Metrics class at all.

Key takeaways

  • A class initializes on first real use, exactly once, under a lock. Compile-time constants don't count as a use.
  • A failed static initializer breaks the class until restart, so keep static setup simple.
  • Static setup runs once, parent first; every object is then built from the parent down.
  • Eager, holder, enum and double-checked locking each have a place; volatile is required for double-checked locking.
  • Reflection, deserialization and separate class loaders can each create a second "single" instance.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

When a class initializes

  • On first active use: new, a static method call, or reading a non-constant static field. A subclass initializes its parent first.
  • Exactly once, under a per-class lock: other threads wait, then see finished statics.
  • Not when you read a compile-time constant (static final primitive or String with a constant value): it's copied into your code.
  • Class.forName(name) initializes; loader.loadClass(name) and Class.forName(name, false, loader) only load.

Order for new Child()

  1. Base static setup, then Child static setup (first time only).
  2. Base field initializers and instance blocks, then the Base constructor.
  3. Child field initializers and instance blocks, then the Child constructor.

Failed static initializer

First use: ExceptionInInitializerError (wrapping the cause). Every later use: NoClassDefFoundError: Could not initialize class X, until the JVM restarts. Keep static setup trivial.

Singleton idioms

private static final Eager INSTANCE = new Eager();                     // eager: simple, built on any static use
private static class Holder { static final Lazy INSTANCE = new Lazy(); }   // holder: lazy, lock-free
enum Registry { INSTANCE; }                                            // enum: blocks reflection and serialization twins
private static volatile Checked instance;                             // double-checked locking: lazy, retryable
NeedPick
Simplesteager
Lazy, creation can't failholder
Strongest against extra instancesenum
Lazy, creation may fail and be retrieddouble-checked locking with volatile, or a synchronized method

How a singleton gets a twin

  • Reflection: setAccessible(true) on the private constructor (enums refuse: "Cannot reflectively create enum objects").
  • Deserialization: add readResolve() returning INSTANCE (enums deserialize by name to the same constant).
  • Class loaders: one copy per loader; nothing defends against it.

Staff-level points, one line each

  • Phases: load, link (verify, prepare statics to defaults, resolve), initialize (<clinit>).
  • A class is (name + defining loader). "X cannot be cast to X" means two loaders. Parent delegation stops you replacing java.lang.String.
  • Class loader leaks after redeploys fill Metaspace (ThreadLocals, driver registration, static references).
  • Two threads initializing two classes that need each other can deadlock in <clinit> (threads show RUNNABLE); one thread just sees a default 0.
  • Changing a public constant in a library doesn't reach callers until they recompile.
  • Double-checked locking was broken before Java 5 (JSR-133); volatile now publishes safely; VarHandle acquire/release is the lighter option; StableValue is a JDK 25 preview.
  • Singletons are globals: they hurt tests. Dependency injection gives "one instance" without the global.

Say it in an interview

  • "I'd use the holder idiom: lazy, thread-safe through class initialization, and no lock after the first call."
  • "If construction can fail, a holder would poison the class, so I'd use double-checked locking with a volatile field."
  • "The enum version also stops reflection and deserialization from making a second instance."

Practice

This skill comes with 14 multiple-choice questions that tell you which misconception a wrong answer reveals and 1 graded problem. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Java: Class Initialization and Singletons free

A Lazy, Retryable Config

MediumProCoding problemJava

A payments service in Dublin loads its settings from a config server through one shared Config object. Building it is slow (it calls the server), and sometimes the server is briefly down. Write Config.getInstance() so that:

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Java: Class Initialization and Singletons free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime