Skip to main content

Section 8.8 Constructors and the super Keyword

Subsection 8.8.1 Constructor Chaining: Understanding Initialization Order

When creating an object, Java requires that the superclass constructor runs before the subclass constructor. This ordering ensures that all inherited fields from the superclass are properly initialized before the subclass adds its specific functionality.
For example, consider a game entity hierarchy:
  • The superclass Entity initializes common fields such as health and name.
  • The subclass Monster might add specific fields, like whether it is aggressive or passive.
If the superclass constructor didn’t run first, subclass methods might accidentally access uninitialized superclass fields, causing runtime errors or unpredictable behavior. Java strictly enforces that superclass initialization occurs before subclass initialization precisely to prevent these issues.
Here’s a concrete example illustrating what could go wrong if initialization order were reversed:
public class Entity {
    protected int health;
    
    public Entity(int initialHealth) {
        health = initialHealth;
    }
    
    public void printHealth() {
        System.out.println("Health: " + health);
    }
}

public class Monster extends Entity {
    private boolean isAggressive;
    
    // Hypothetical situation if initialization order were reversed
    public Monster(int initialHealth, boolean aggressive) {
        // Imagine if Monster's initialization ran first
        isAggressive = aggressive;
        
        // The Monster might call methods using superclass fields
        printHealth();  // Would access uninitialized 'health' field!
        
        // Only after this would the superclass constructor run
        // super(initialHealth);  
    }
}
In this hypothetical scenario, printHealth() would be called before health is initialized by the superclass constructor, potentially causing a runtime error or printing an incorrect default value (0). By enforcing that superclass constructors run first, Java prevents these subtle initialization bugs.

Subsection 8.8.2 Explicit Calls to the Superclass Constructor with super()

In Java, subclass constructors must always invoke a constructor from their superclass. Java implicitly calls a superclass constructor only if the superclass provides a no-argument constructor. If the superclass does not have a no-argument constructor, you must explicitly call its parameterized constructor using the super(...) keyword. Failing to do so results in a compile-time error.
The syntax is straightforward:
public SubclassName(parameters) {
    super(arguments);  // must be first line
    // subclass-specific initialization
}
Important notes:
  • The call to super(...) must be the first line in the subclass constructor.
  • If you omit super(...) when the superclass requires parameters, Java will issue a compile-time error.
Here’s a concrete, correct example:
// Superclass: Entity.java
public class Entity {
    protected int health;

    public Entity(int initialHealth) {
        health = initialHealth;
    }
}

// Subclass: Monster.java
public class Monster extends Entity {
    private boolean isAggressive;

    public Monster(int initialHealth, boolean aggressive) {
        super(initialHealth);  // Explicit call required
        isAggressive = aggressive;
    }
}
If you forget to include the call to super(initialHealth), Java produces a clear compile-time error:
Error: constructor Entity() not found

Subsection 8.8.3 Visualizing Constructor Chaining

To help visualize constructor chaining, let’s enhance our previous example with explicit print statements. Observe carefully the order in which the constructors execute:
// Superclass: Entity.java
public class Entity {
    protected int health;

    public Entity(int initialHealth) {
        health = initialHealth;
        System.out.println("Entity constructor: health set to " + health);
    }
}

// Subclass: Monster.java
public class Monster extends Entity {
    private boolean isAggressive;

    public Monster(int initialHealth, boolean aggressive) {
        super(initialHealth);
        isAggressive = aggressive;
        System.out.println("Monster constructor: isAggressive set to " + isAggressive);
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        Monster goblin = new Monster(100, true);
    }
}
Running this code results in the following console output:
Entity constructor: health set to 100
Monster constructor: isAggressive set to true
This demonstrates explicitly that Java executes the superclass constructor first, initializing inherited fields, before running the subclass constructor.

Subsection 8.8.4 Common Pitfalls with Constructors and super()

Here are some frequent mistakes students encounter with constructors in inheritance hierarchies:
  • Omitting explicit super() when required: If the superclass lacks a no-argument constructor, forgetting to explicitly call super(...) leads to a compilation error.
  • Incorrect placement of super(): The super(...) call must always be the very first statement. Placing any other statement before it causes a compile-time error.
  • Calling overridable methods in constructors: This can lead to subtle bugs when subclasses override those methods before their fields are initialized.
  • Overly complex constructor chains: Long chains of constructor calls across multiple levels of inheritance can make code difficult to understand and maintain.
Here’s an example of incorrect constructor chaining that Java will reject:
// Incorrect: will not compile
public class Monster extends Entity {
    private boolean isAggressive;

    public Monster(int initialHealth, boolean aggressive) {
        isAggressive = aggressive;  // Error: code before super()
        super(initialHealth);
    }
}
Compiler error:
Error: Call to super() must be first statement in constructor
Corrected example:
// Corrected version
public class Monster extends Entity {
    private boolean isAggressive;

    public Monster(int initialHealth, boolean aggressive) {
        super(initialHealth);  // Correct placement
        isAggressive = aggressive;
    }
}
Here’s another subtle issue to avoid - calling overridable methods from constructors:
// Dangerous practice: calling overridable methods in constructor
public class Entity {
    protected int health;

    public Entity(int initialHealth) {
        health = initialHealth;
        initializeEntity();  // Dangerous if overridden by subclasses
    }
    
    protected void initializeEntity() {
        System.out.println("Entity initialized with health: " + health);
    }
}

public class Monster extends Entity {
    private int power;  // Not yet initialized when superclass constructor runs!

    public Monster(int initialHealth) {
        super(initialHealth);
        power = initialHealth / 2;
    }
    
    @Override
    protected void initializeEntity() {
        System.out.println("Monster initialized with health: " + health);
        System.out.println("Monster power ratio: " + (power / health));  // Bug! power is still 0
    }
}
In this example, when the Entity constructor calls initializeEntity(), Java uses the Monster version of the method (due to polymorphism). However, the Monster constructor hasn’t yet run, so the power field is still at its default value of 0, potentially causing a division by zero error or incorrect calculations.

Subsection 8.8.5 Constructor Design Best Practices

Designing constructors in inheritance hierarchies requires care to maintain clarity, safety, and ease of maintenance. Here are key best practices:
  • Keep constructors simple: Constructors should primarily focus on initializing fields, not performing complex operations or business logic.
  • Provide sensible defaults: Include constructors with reasonable default values to make subclass creation easier.
  • Use constructor overloading carefully: Multiple constructors can improve usability but should be managed to avoid confusion.
  • Avoid calling overridable methods: As seen in the previous section, this can lead to unpredictable behavior.
  • Document constructor requirements: Clearly document what parameters are required for subclass constructors.
Here’s an example of a well-designed constructor hierarchy:
public class Entity {
    protected String name;
    protected int health;
    protected int maxHealth;
    
    // Constructor with all parameters
    public Entity(String name, int maxHealth) {
        this.name = name;
        this.maxHealth = maxHealth;
        this.health = maxHealth;  // Start at full health
    }
    
    // Constructor with default health
    public Entity(String name) {
        this(name, 100);  // Default max health of 100
    }
}

public class Monster extends Entity {
    private boolean isAggressive;
    private int attackPower;
    
    // Full constructor
    public Monster(String name, int maxHealth, int attackPower, boolean isAggressive) {
        super(name, maxHealth);
        this.attackPower = attackPower;
        this.isAggressive = isAggressive;
    }
    
    // Simpler constructor with good defaults
    public Monster(String name, int maxHealth) {
        this(name, maxHealth, maxHealth / 4, true);  // Default attack = 1/4 of max health, aggressive by default
    }
    
    // Minimal constructor
    public Monster(String name) {
        super(name);  // Uses parent's default health of 100
        this.attackPower = 25;  // Default attack power
        this.isAggressive = true;  // Aggressive by default
    }
}
This design provides flexibility while maintaining simplicity:
  • Multiple constructors cater to different creation scenarios
  • Sensible defaults reduce the burden on developers creating subclasses
  • Each constructor is focused only on initialization, not complex logic
  • The constructors use chaining to avoid code duplication

Insight 8.8.1. The Fragile Base Class Problem and Constructors.

One important reason to keep constructors simple and avoid overridable methods in them relates to the "fragile base class problem." This occurs when changes to a base class unexpectedly break subclasses.
With constructors, this fragility is amplified because:
  • Subclasses must work with whatever initialization the superclass performs
  • Changes to superclass constructor behavior affect all subclasses, sometimes in subtle ways
  • The initialization sequence is fixed and cannot be modified by subclasses
By keeping constructors simple and predictable, you reduce the risk of breaking subclasses when the superclass evolves.
Check Your Understanding

Checkpoint 8.8.2.

Which of the following is a valid way to call a superclass constructor from a subclass?
  • super(...);
  • this.super();
  • Superclass();
  • parent();

Checkpoint 8.8.3. Understanding Constructor Chaining.

Which statement correctly describes how Java executes constructors when creating a subclass object?
  • The superclass constructor always executes first, followed by the subclass constructor.
  • Correct! Java enforces superclass initialization first to ensure that inherited fields are initialized before the subclass.
  • The subclass constructor executes first, followed by the superclass constructor.
  • Incorrect. If subclass constructors ran first, inherited fields could remain uninitialized, causing errors.
  • Only the subclass constructor runs, and superclass initialization is optional.
  • Incorrect. Java requires superclass initialization to ensure inherited fields are properly set.

Checkpoint 8.8.4.

What happens if a subclass constructor does not explicitly call super(...) when the superclass lacks a no-argument constructor?
  • A compilation error occurs.
  • The compiler automatically inserts a call to the superclass’s parameterized constructor.
  • The subclass compiles successfully but throws an error at runtime.
  • The subclass is forced to define its own no-argument constructor.

Checkpoint 8.8.5.

Where must the super(...) call be placed in a constructor?
  • As the very first statement inside the constructor.
  • Anywhere inside the constructor.
  • As the last statement inside the constructor.
  • It is optional and can be omitted in all cases.

Checkpoint 8.8.6.

What happens if a constructor tries to call super(...) but does so after another statement?
  • The program will still compile, but super(...) will be ignored.
  • The constructor will execute normally, but super(...) will run last.
  • A compile-time error will occur.
  • Java will automatically reorder the statements to place super(...) first.

Checkpoint 8.8.7.

If a subclass does not define any constructors, what happens?
  • It automatically inherits the superclass’s constructors.
  • The compiler generates a default constructor that calls super().
  • It will fail to compile unless a constructor is explicitly provided.
  • The subclass cannot be instantiated.
You have attempted of activities on this page.