The Real Initialization Order of Java Classes (And a Subtle Pitfall)
Most Java developers “know” the basic initialization order of a class:
- Call the base class constructor (and repeat this up the inheritance chain)
- Initialize member fields in the order of declaration
- 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);
}
}
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
Key observations:
-
Animal’s constructor callswalk(). - Because
walk()is overridden inCat,Cat.walk()is invoked. - But when
Cat.walk()is called,stepis 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:
-
Memory allocation & zeroing
When you create a new object (e.g.
new Cat(500)), the JVM:- Allocates memory for the full object (including
AnimalandCatparts) - Initializes that memory to binary zero
- For primitives (
int,long, etc.), that means0 - For references, that means
null
- For primitives (
- Allocates memory for the full object (including
At this moment, step is 0.
-
Base class constructor chain
BeforeCat’s constructor body runs, Java must call the base class constructor(s):-
Animal()runs first. - Inside
Animal()we callwalk(). - Due to dynamic dispatch,
Cat.walk()is called even though we’re insideAnimal’s constructor. - But remember:
Cat’s own fields haven’t been initialized yet, sostepis still0.
-
-
Field initialization of the current class
After all base constructors complete, Java initializes the fields declared inCat, in the order they appear:-
private int step = 100;runs here. - Now
stepbecomes100.
-
-
Subclass constructor body
Finally, the body ofCat(int step)executes:-
this.step = step;setsstepto500. - Then we print
Cat.Cat(), step = 500.
-
So the full timeline for step is:
- Allocated and zeroed →
step = 0 -
Animal()callswalk()→ printsstep = 0 - Field initializer runs →
step = 100 -
Catconstructor 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
privateorfinalto 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
thisto 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:
-
finalmethods 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)