Loading learning content...
When you write new DerivedClass(), you trigger a carefully orchestrated sequence of events. Fields are initialized, constructors are called, static blocks may execute, and each level of the inheritance hierarchy participates in a specific order.
Understanding this initialization order isn't academic trivia—it prevents real bugs. Developers who don't understand the sequence write code that accesses uninitialized fields, calls methods before objects are ready, or creates inconsistent state that fails mysteriously later.
This page reveals the complete picture: exactly what happens, in what order, when an object is born.
By the end of this page, you'll understand the complete initialization sequence: class loading, static initialization, instance field initialization, constructor execution order, and the critical moments where things can go wrong.
Object initialization in an inheritance hierarchy follows a multi-phase process. Understanding the phases helps you reason about when specific code runs.
The Three Major Phases:
| Phase | When | What Happens | Order in Hierarchy |
|---|---|---|---|
| Static Initialization | First use of class | Static fields initialized, static blocks run | Base → Derived (if not already loaded) |
| Memory Allocation | When 'new' is called | Heap space allocated for object | Single allocation for entire object |
| Instance Initialization | During 'new' execution | Instance fields + constructors run | Base → Derived (constructor bodies) |
The Key Insight:
The most derived class's constructor starts execution first, but it immediately calls its parent's constructor (explicitly or implicitly), which calls its parent, and so on. Constructors start in derived-to-base order but complete in base-to-derived order.
This means: by the time your constructor body runs, all parent constructors have already completed.
Before any instances can be created, the class itself must be loaded and its static members initialized. This happens once per class, the first time the class is referenced.
Static Initialization Order:
1234567891011121314151617181920212223242526272829303132333435
// Demonstrating static initialization order// Note: TypeScript doesn't have static blocks like Java, but we can simulate class BaseClass { static baseStaticField: string = (() => { console.log("1. BaseClass: static field initializer"); return "base-static"; })(); // In languages with static blocks (Java, new JavaScript): // static { console.log("2. BaseClass: static block"); }} class DerivedClass extends BaseClass { static derivedStaticField: string = (() => { console.log("2. DerivedClass: static field initializer"); return "derived-static"; })();} // First reference to DerivedClass triggers static initializationconsole.log("=== First access to DerivedClass ===");console.log(DerivedClass.derivedStaticField); // Output:// 1. BaseClass: static field initializer (parent static first)// 2. DerivedClass: static field initializer (child static second)// === First access to DerivedClass ===// derived-static // Second access - statics already initializedconsole.log("=== Second access ===");console.log(DerivedClass.derivedStaticField);// Output: derived-static (no initializers run - already done)Static initialization happens once per class per program execution (or classloader in Java). It happens before ANY instance exists. Don't confuse static initialization with instance initialization—they're completely separate phases.
Instance field initialization is interleaved with constructor execution in a specific way. The exact timing depends on the language, but the principle is consistent: parent fields are initialized before child fields.
The Critical Sequence (Java-style, which most languages approximate):
For each class in the hierarchy, from base to derived:
123456789101112131415161718192021222324252627282930313233343536373839
// Instance field initialization order demonstration class Parent { // Field initializer runs before constructor body parentField: string = (() => { console.log("2. Parent: field initializer"); return "parent-value"; })(); constructor() { console.log("3. Parent: constructor body, parentField =", this.parentField); }} class Child extends Parent { // Child's field initializer runs AFTER parent constructor completes childField: string = (() => { console.log("4. Child: field initializer"); return "child-value"; })(); constructor() { console.log("1. Child: constructor entry (before super)"); super(); // Triggers all of parent's initialization console.log("5. Child: constructor body, childField =", this.childField); }} console.log("=== Creating Child instance ===");const child = new Child(); /* Output:=== Creating Child instance ===1. Child: constructor entry (before super)2. Parent: field initializer3. Parent: constructor body, parentField = parent-value4. Child: field initializer5. Child: constructor body, childField = child-value*/Let's put together the complete picture for a three-level hierarchy. This sequence is what actually happens in memory when you write new GrandChild().
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
// Complete initialization sequence for a 3-level hierarchy class GrandParent { gpField: string = this.initField("GrandParent field"); constructor() { console.log(" GrandParent: constructor body"); } private initField(name: string): string { console.log(` ${name} initialized`); return name; }} class Parent extends GrandParent { parentField: string = this.initField("Parent field"); constructor() { console.log(" Parent: before super()"); super(); console.log(" Parent: after super(), before body"); console.log(" Parent: constructor body"); } private initField(name: string): string { console.log(` ${name} initialized`); return name; }} class Child extends Parent { childField: string = this.initField("Child field"); constructor() { console.log(" Child: before super()"); super(); console.log(" Child: after super(), before body"); console.log(" Child: constructor body"); } private initField(name: string): string { console.log(` ${name} initialized`); return name; }} console.log("Creating Child instance:");console.log("-".repeat(40));new Child(); /* Output:Creating Child instance:---------------------------------------- Child: before super() Parent: before super() GrandParent field initialized GrandParent: constructor body Parent field initialized Parent: after super(), before body Parent: constructor body Child field initialized Child: after super(), before body Child: constructor body*/| Step | What Happens | At This Point |
|---|---|---|
| 1 | Child constructor called | Nothing initialized yet |
| 2 | Child calls super() → Parent constructor | Control moves to Parent |
| 3 | Parent calls super() → GrandParent constructor | Control moves to GrandParent |
| 4 | GrandParent field initializers run | GrandParent fields ready |
| 5 | GrandParent constructor body runs | GrandParent fully initialized |
| 6 | Parent field initializers run | Parent fields ready |
| 7 | Parent constructor body runs | Parent fully initialized |
| 8 | Child field initializers run | Child fields ready |
| 9 | Child constructor body runs | Object fully initialized |
The initialization sequence creates several dangerous moments where things can go wrong. Understanding these helps you write safer constructors.
The Core Problem:
During initialization, the object exists but isn't fully ready. Accessing fields or methods at the wrong time leads to seeing default/null values, calling methods on uninitialized state, or violating invariants that the constructor was supposed to establish.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// DANGER 1: Calling overridable methods in constructorclass UnsafeParent { protected value: number = 0; constructor() { this.setup(); // DANGEROUS: Calls overridden version! } protected setup(): void { console.log("Parent setup"); this.value = 10; }} class UnsafeChild extends UnsafeParent { private multiplier: number = 5; // Not initialized when parent constructor runs! protected setup(): void { console.log("Child setup, multiplier =", this.multiplier); // BUG: multiplier is undefined here because child field // initializers haven't run yet when parent constructor calls setup() this.value = 10 * this.multiplier; // 10 * undefined = NaN }} const unsafe = new UnsafeChild();console.log("Final value:", unsafe["value"]); // NaN! // DANGER 2: Passing 'this' out during constructionclass LeakingThis { constructor(registry: Map<string, LeakingThis>) { console.log("LeakingThis: registering self"); registry.set("instance", this); // DANGEROUS: Object not fully constructed! // Other code might access this half-constructed object }} // DANGER 3: Starting threads/async operations in constructorclass AsyncUnsafe { private data: string[] = []; constructor() { // DANGEROUS: Async operation may complete after constructor // but before the object is fully ready setTimeout(() => { // By now the object MIGHT be ready, or it might not be // Depends on what happens after constructor this.processData(); }, 0); } processData(): void { console.log("Processing", this.data); }}Never call overridable methods from a constructor, never pass 'this' to external code during construction, and never start async operations in constructors. The object isn't ready, and you'll create subtle, hard-to-debug problems.
Given the dangers, how do we safely initialize complex objects? Several patterns help.
Pattern 1: Factory Methods
Construct the object, then call initialization methods after construction is complete.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
// PATTERN 1: Factory Methodclass SafeComponent { private initialized: boolean = false; private eventHandlers: Map<string, Function> = new Map(); // Private constructor - can't be called directly private constructor() { // Only set up fields, don't call methods } // Factory method ensures complete initialization static create(): SafeComponent { const component = new SafeComponent(); component.initialize(); // Called AFTER constructor completes return component; } private initialize(): void { // Safe to call any methods now this.setupEventHandlers(); this.registerWithGlobalRegistry(); this.initialized = true; } private setupEventHandlers(): void { /* ... */ } private registerWithGlobalRegistry(): void { /* ... */ }} // Usageconst component = SafeComponent.create(); // PATTERN 2: Two-Phase Initializationclass TwoPhaseWidget { private ready: boolean = false; constructor() { // Phase 1: Basic field setup only } // Phase 2: Complete initialization (called externally) initialize(): void { if (this.ready) return; // Idempotent this.doComplexSetup(); this.ready = true; } private doComplexSetup(): void { /* ... */ } // Guard methods performAction(): void { if (!this.ready) { throw new Error("Widget not initialized. Call initialize() first."); } // ... actual logic }} // PATTERN 3: Builder Pattern for Complex Objectsclass ComplexObject { constructor( readonly field1: string, readonly field2: number, readonly field3: boolean, readonly optionalField?: string ) { // All fields provided - no danger of partial initialization }} class ComplexObjectBuilder { private field1?: string; private field2?: number; private field3?: boolean; private optionalField?: string; setField1(value: string): this { this.field1 = value; return this; } setField2(value: number): this { this.field2 = value; return this; } setField3(value: boolean): this { this.field3 = value; return this; } setOptionalField(value: string): this { this.optionalField = value; return this; } build(): ComplexObject { // Validate before construction if (!this.field1 || this.field2 === undefined || this.field3 === undefined) { throw new Error("Required fields not set"); } return new ComplexObject( this.field1, this.field2, this.field3, this.optionalField ); }} // Usageconst obj = new ComplexObjectBuilder() .setField1("hello") .setField2(42) .setField3(true) .build();Factory methods are good for moderate complexity. Two-phase initialization works when construction and initialization must be separated (e.g., dependency injection). Builders are best for objects with many optional fields or complex validation.
Different languages handle initialization order slightly differently. Understanding these differences is crucial when working across languages.
| Aspect | Java | TypeScript/JavaScript | Python | C++ |
|---|---|---|---|---|
| Field initializers | Before constructor body | Before constructor body | After new, before init | In initializer list |
| super() must be first? | Yes | Yes (before 'this') | No (but conventional) | Yes (in initializer list) |
| Virtual calls in ctor | Calls overridden (dangerous) | Calls overridden | Calls overridden | Calls base version (safe) |
| Static initialization | Class load time | Module load time | Module load time | Before main() or first use |
12345678910111213141516171819202122232425262728293031323334353637383940
// C++ difference: virtual calls in constructor DON'T call overrides// This is actually SAFER than Java/TypeScript! #include <iostream> class Base {public: Base() { speak(); // In C++, this calls Base::speak(), NOT Derived::speak() } virtual void speak() { std::cout << "Base speaks" << std::endl; }}; class Derived : public Base {private: std::string name; public: Derived() : Base(), name("Derived") { speak(); // This calls Derived::speak() } void speak() override { std::cout << name << " speaks" << std::endl; }}; int main() { Derived d; // Output: // Base speaks (from Base constructor - calls Base::speak) // Derived speaks (from Derived constructor - calls Derived::speak) return 0;} // C++ is SAFER here because the Base constructor can't accidentally// call Derived's override when Derived isn't ready yet.C++ constructors calling virtual methods is actually safe—the call resolves to the current class under construction, not the most-derived override. This prevents the uninitialized-field bugs possible in Java, TypeScript, and Python. It's one area where C++'s complexity provides safety.
When initialization goes wrong, the symptoms can be mysterious: null pointer exceptions, unexpected default values, or invariant violations that "shouldn't be possible." Here's how to debug.
Debugging Strategies:
123456789101112131415161718192021222324252627282930313233343536373839
// Debugging technique: Add initialization tracing class TracedBase { private baseField: string; constructor() { console.log(`[${this.constructor.name}] Base constructor START`); console.log(`[${this.constructor.name}] Base: initializing baseField`); this.baseField = "base"; console.log(`[${this.constructor.name}] Base constructor END`); }} class TracedChild extends TracedBase { private childField: string; constructor() { console.log(`[${this.constructor.name}] Child constructor START`); console.log(`[${this.constructor.name}] Calling super()...`); super(); console.log(`[${this.constructor.name}] super() returned`); console.log(`[${this.constructor.name}] Initializing childField`); this.childField = "child"; console.log(`[${this.constructor.name}] Child constructor END`); }} new TracedChild(); /* Output helps visualize the order:[TracedChild] Child constructor START[TracedChild] Calling super()...[TracedChild] Base constructor START[TracedChild] Base: initializing baseField[TracedChild] Base constructor END[TracedChild] super() returned[TracedChild] Initializing childField[TracedChild] Child constructor END*/Understanding initialization order is essential for writing correct inheritance hierarchies. Let's consolidate the key insights:
You've completed the module on the super keyword and chain calls! You now understand: accessing parent members, constructor chaining, method chaining patterns, and the complete initialization order. This knowledge forms the foundation for safe, correct inheritance hierarchies.
Module Recap:
super.membersuper()With these concepts mastered, you're ready to explore inheritance hierarchy design considerations—how to structure hierarchies that remain maintainable and avoid the common pitfalls of deep inheritance.