DEV Community

Thellu
Thellu

Posted on

The *Real* Initialization Order of Java Classes (And a Subtle Pitfall)

The Real Initialization Order of Java Classes (And a Subtle Pitfall)

Most Java developers “know” the basic initialization order of a class:

  1. Call the base class constructor (and repeat this up the inheritance chain)
  2. Initialize member fields in the order of declaration
  3. Execute the body of the class constructor

This mental model works in many cases—but it’s incomplete.

In some situations, especially when inheritance and overridden methods are involved, this understanding can lead to very surprising behavior.

Let’s look at a concrete example.


A Surprising Example

Consider the following code:

class Animal {

    public void walk() {
        System.out.println("Animal.walk()");
    }

    Animal() {
        System.out.println("Animal() before walk()");
        walk();
        System.out.println("Animal() after walk()");
    }
}

class Cat extends Animal {
    private int step = 100;

    @Override
    public void walk() {
        System.out.println("Cat.walk(), step = " + step);
    }

    Cat(int step) {
        this.step = step;
        System.out.println("Cat.Cat(), step = " + step);
    }
}

public class Main {
    public static void main(String[] args) {
        new Cat(500);
    }
}
Enter fullscreen mode Exit fullscreen mode

At a glance, you might expect the output to use step = 100 at least once, since that’s the field’s default value in Cat.

But here’s what actually gets printed:

Animal() before walk()
Cat.walk(), step = 0
Animal() after walk()
Cat.Cat(), step = 500
Enter fullscreen mode Exit fullscreen mode

Key observations:

  • Animal’s constructor calls walk().
  • Because walk() is overridden in Cat, Cat.walk() is invoked.
  • But when Cat.walk() is called, step is 0, not 100, and not 500.

So what’s going on?


What Actually Happens During Initialization

To understand this, we need a more accurate mental model of Java’s object initialization process.

The real steps are roughly:

  1. Memory allocation & zeroing When you create a new object (e.g. new Cat(500)), the JVM:
    • Allocates memory for the full object (including Animal and Cat parts)
    • Initializes that memory to binary zero
      • For primitives (int, long, etc.), that means 0
      • For references, that means null

At this moment, step is 0.

  1. Base class constructor chain

    Before Cat’s constructor body runs, Java must call the base class constructor(s):

    • Animal() runs first.
    • Inside Animal() we call walk().
    • Due to dynamic dispatch, Cat.walk() is called even though we’re inside Animal’s constructor.
    • But remember: Cat’s own fields haven’t been initialized yet, so step is still 0.
  2. Field initialization of the current class

    After all base constructors complete, Java initializes the fields declared in Cat, in the order they appear:

    • private int step = 100; runs here.
    • Now step becomes 100.
  3. Subclass constructor body

    Finally, the body of Cat(int step) executes:

    • this.step = step; sets step to 500.
    • Then we print Cat.Cat(), step = 500.

So the full timeline for step is:

  • Allocated and zeroed → step = 0
  • Animal() calls walk() → prints step = 0
  • Field initializer runs → step = 100
  • Cat constructor body runs → step = 500

That’s why you see step = 0 in the output.


Why This Is Dangerous

This behavior leads to a subtle but important rule:

Never rely on subclass state inside a base class constructor.

Because:

  • The base class constructor can (and often does) use virtual methods.
  • Those methods might be overridden in subclasses.
  • If they are, they might access fields that are not initialized yet.
  • This can cause incorrect behavior that’s very hard to detect and debug.

In our example, Animal() “innocently” calls walk(), but that ends up invoking Cat.walk(), which uses step before it’s properly initialized.


Safe Practices for Constructors

To avoid this category of bugs, you can follow these guidelines:

1. Avoid calling overridable methods in constructors

  • Don’t call methods that can be overridden (public, protected, or package-private non-final methods).
  • If you must call a method from the constructor, make it private or final to prevent overriding.

2. Base class constructors should only use base-class state

  • Use only fields defined in the base class.
  • Don’t “assume” anything about subclass fields or behavior.

3. Initialize all fields before using them

  • Be careful not to pass this to other objects from inside your constructor if they might immediately call overridable methods on it.

4. Final methods in base classes are safe

The only virtual methods that are “safe” to call in a constructor are:

  • final methods in the base class

Because they cannot be overridden, the behavior is fixed and can’t accidentally depend on uninitialized subclass state.


Top comments (0)