JAVA / INHERITANCE AND POLYMORPHISM
super for reusing parent constructors and methods
Chain constructors with super(...) and extend inherited behavior with super.method(), knowing exactly when each runs and why super never re-dispatches.
What you will learn
- Call super(args) first in a constructor to choose which parent constructor runs
- Predict initialization order: parent constructor body finishes before the child's begins
- Wrap super.method() inside an override to extend instead of replacing behavior
- Explain why super.method() is bound at compile time and cannot cause infinite recursion
Understanding super for reusing parent constructors and methods
The keyword super does two unrelated jobs. As a call, super(...) picks which of the parent's constructors runs before the subclass constructor body; as a qualifier, super.describe() calls the parent's version of a method the subclass has overridden. The constructor form exists because a SavingsAccount object is one object that contains Account's fields inside it, and only Account's own constructor knows how to put those fields into a valid state, so it has to run first.
Every subclass constructor runs a parent constructor, whether you write it or not: if the first statement is neither super(...) nor this(...), the compiler inserts a no-argument super(). That is why a parent declaring only Account(String, double) breaks any subclass constructor that forgets to delegate explicitly, since the inserted super() has no matching constructor to call. Reading the chain top down also explains the print order below: Account's body completes before rate is assigned, so a subclass can never see its own fields already set while the parent is still constructing.
super.describe() compiles to a non-virtual call to the implementation in the immediate superclass, and that single fact is what makes the wrapping pattern work. Inside SavingsAccount.describe(), a plain describe() would dispatch back to SavingsAccount.describe() and recurse until the stack overflows, while super.describe() is bound at compile time to Account.describe() regardless of the object's runtime class. The same qualifier reaches a parent field that the subclass hides, as in super.count, which is the only way to read it once the subclass declares a field with the same name.
Placeholder
class Account {
private final String id;
private double balance;
Account(String id, double balance) {
this.id = id;
this.balance = balance;
System.out.println("Account constructor ran for " + id);
}
String describe() {
return "Account " + id + " holds " + balance;
}
}
class SavingsAccount extends Account {
private final double rate;
SavingsAccount(String id, double balance, double rate) {
super(id, balance);
this.rate = rate;
System.out.println("SavingsAccount constructor ran with rate " + rate);
}
@Override
String describe() {
return super.describe() + " at " + rate + "% interest";
}
}
public class Main {
public static void main(String[] args) {
System.out.println(new SavingsAccount("SV-1", 250.0, 1.5).describe());
}
}A subclass constructor must run one parent constructor first through super(...), and super.member reaches the parent's implementation without going through virtual dispatch.
Worked examples
Implicit super() versus a chosen parent constructor
Shows that a missing super call becomes super(), while super(6) selects a different parent constructor.
class Vehicle {
Vehicle() {
System.out.println("Vehicle()");
}
Vehicle(int wheels) {
System.out.println("Vehicle(" + wheels + ")");
}
}
class Bike extends Vehicle {
Bike() {
System.out.println("Bike()");
}
}
class Truck extends Vehicle {
Truck() {
super(6);
System.out.println("Truck()");
}
}
public class Main {
public static void main(String[] args) {
new Bike();
new Truck();
}
}Example explained
Line 1Bike() writes no super call, so the compiler inserts super() and Vehicle() runs before Bike's own line.
Line 2Truck() writes super(6), which selects Vehicle(int) instead, so Vehicle() is never invoked for a Truck.
Line 3In both pairs the parent line prints first, confirming the parent constructor completes before the child body starts.
super is fixed to the immediate parent, not the runtime class
Demonstrates that super.render() in a three-level chain is resolved at compile time, so the wrapping calls terminate.
class Widget {
String render() {
return "widget";
}
}
class Button extends Widget {
@Override
String render() {
return "[" + super.render() + "]";
}
}
class IconButton extends Button {
@Override
String render() {
return "@" + super.render();
}
}
public class Main {
public static void main(String[] args) {
Widget w = new IconButton();
System.out.println(w.render());
System.out.println(new Button().render());
}
}Example explained
Line 1w.render() is a virtual call, so it starts at IconButton.render() even though w is declared as Widget.
Line 2super.render() inside IconButton is bound to Button.render() when compiled, so it does not come back to IconButton and loop.
Line 3Button's own super.render() reaches Widget.render(), which supplies the innermost text "widget".
Line 4The second line prints without the @ prefix because a plain Button has no IconButton layer to add it.
What the parent constructor can and cannot see
Shows that a method called during super() dispatches to the subclass override before the subclass fields are initialized.
class Base {
Base() {
System.out.println("Base ctor sees: " + describe());
}
String describe() {
return "base";
}
}
class Derived extends Base {
private String name = "derived";
Derived() {
super();
System.out.println("Derived ctor sees: " + describe());
}
@Override
String describe() {
return "name=" + name;
}
}
public class Main {
public static void main(String[] args) {
new Derived();
}
}Example explained
Line 1super() runs Base's body first, and describe() there already dispatches to the Derived override.
Line 2The field initializer name = "derived" has not run yet, so name still holds its default null.
Line 3After super() returns, field initializers run, so the second call to the same method returns name=derived.
Line 4This is why calling an overridable method from a constructor is unsafe once someone extends the class.
Important notes
There is no super.super.describe(); you cannot skip a level, so if a grandchild needs the grandparent's behavior the middle class must expose it under another name.
Java 25 finalized flexible constructor bodies, allowing statements before super(...), but that prologue still may not read fields or call instance methods of the object being constructed.
Common mistakes
Validating an argument before delegating, such as an if-throw above super(id, balance): on Java 24 and earlier the compiler rejects it with "call to super must be first statement in constructor"; move the check into a static helper used inside the argument expression.
Omitting super(id, balance) when the parent declares no no-argument constructor: the compiler silently inserts super() and reports that Account cannot be applied to given types, with the error pointing at the subclass constructor line that looks correct.
Writing describe() or this.describe() instead of super.describe() inside the override: the call dispatches straight back into the override and the program dies at runtime with StackOverflowError rather than failing to compile.
Try it yourself
Change, predict, then run
Write a Shape class whose constructor stores a name and whose describe() returns it, then add Circle extends Shape with a constructor that calls super("circle") and stores a radius, and an override returning super.describe() plus the radius. Delete the super call and read the exact compiler error before putting it back.
Open the Java workspaceCheck your understanding
Widget is extended by Button, which is extended by IconButton, and Button.render() calls super.render(). If you create an IconButton and call render() on it, which implementation does the super.render() inside Button.render() reach?
- Widget.render(), because super.render() is bound to the immediate superclass at compile time
- IconButton.render(), because the object's runtime type is IconButton and calls are virtual
- Button.render() again, so the call recurses until a StackOverflowError
- Widget.render() or IconButton.render(), depending on whether the variable is declared as Widget or IconButton
Show answer
super.render() is resolved against the superclass of the class that lexically contains the call, so inside Button it always means Widget.render(), and it is emitted as a non-virtual invocation. Option 1 is tempting because a plain render() or this.render() in that spot really would dispatch to IconButton.render() and recurse forever; super is deliberately the opposite of a virtual call, which is exactly why the wrapping idiom terminates. The declared type of the variable never affects which body runs.