The super keyword in Java
The three jobs of super: chaining to a parent constructor, calling the parent's method and reading a hidden field, plus initialisation order, invokespecial, Interface.super for clashing defaults and where super is forbidden.
On this page
🪜 What is the super keyword?
super lets a subclass reach the parent's version of something it has inherited. It has three jobs: super(...) calls a parent constructor, super.fee() calls the parent's implementation of a method you've overridden, and super.channel reads a parent field your class has hidden. For example, a CardPayment can compute its fee as super.fee() + 150, reusing the base fee and adding a card surcharge.
super looks like a small keyword, but interviewers use it to test whether you understand object construction and method dispatch at the same time. Why does class Savings extends Account {} sometimes fail to compile with an error about a constructor you never wrote? Is super.fee() virtual? Can you write super.super.fee()? What's the order of field initializers and constructor bodies across a hierarchy? Each of these has a precise answer, and the precise answer comes from the bytecode.
What you’ll learn
- How
super(...)chains constructors and the exact initialisation order across a hierarchy - Why the implicit
super()causes a compile error when the parent has no no-arg constructor - Why
super.method()compiles toinvokespecialand is not virtually dispatched, whilethisstays the subclass - How
Interface.super.method()resolves clashingdefaultmethods - Where
superis not allowed (static context,super.super) and what Java 25's flexible constructor bodies change
For this lesson, we'll focus on super(...) and constructor chaining, the implicit super() the compiler inserts, super.method() and super.field, the invokespecial instruction behind them, Interface.super.method() for clashing defaults, and where super isn't allowed.
Below the line (out of scope)
thisandthis(...)constructor chaining within one class: covered in The this keyword- Constructors in general (default constructors, overloading, private constructors): covered in A guide to constructors
- What is and isn't inherited in the first place: covered in A guide to inheritance
The 30-second answer
super(...) calls a parent constructor and must be the first statement of a constructor in Java 21. If you don't write one, the compiler inserts super(), which fails to compile when the parent has no accessible no-arg constructor. super.method() calls the parent's implementation directly: it compiles to invokespecial, so it is not virtually dispatched, though this inside that parent method is still the subclass object. super.field reads a hidden parent field. Interface.super.method() picks one interface's default when two clash. super refers to the current instance, so it can't be used in a static context, and there is no super.super.
Interview radar
Asked very often: Expect questions on super(...) being inserted by the compiler, constructor order, and whether super.method() is virtual.
Senior rounds probe: I probe invokespecial, overridable calls from constructors, Interface.super.method() and why there is no super.super.
Production angle: This appears in broken exception chaining, framework lifecycle overrides that must call super, and constructor code that reads subclass state too early.
01Picture it first: building floors from the foundation up
Think of constructing a three-storey building. The top-floor contractor can't start until the floor below exists, and that floor can't start until the foundation is poured. So the request goes up the chain ("before I build, the floor below must be built"), and the actual work happens down from the foundation. new CardPayment() works the same way: the CardPayment constructor immediately calls Payment's, which immediately calls Object's, and initialisation then runs from Object down to CardPayment.
The figure shows that round trip for one new CardPayment(). Note that there is still only one object; each constructor initialises its own class's slice of it.
Constructor chaining for new CardPayment(): calls climb from CardPayment to Payment to Object, then field initializers and constructor bodies run downward from Payment to CardPayment
02How super works under the hood
super(...) and the order of initialisation
Try it yourself first
Before reading on, write down the four lines this prints, in order. Both classes have a field initializer and a constructor body.
class Payment {
private final long createdAt = log("Payment field initializer");
Payment() { log("Payment constructor body"); }
static long log(String step) { System.out.println(step); return System.nanoTime(); }
}
class CardPayment extends Payment {
private long surcharge = log("CardPayment field initializer");
CardPayment() { log("CardPayment constructor body"); }
}
new CardPayment();
// prints:
// Payment field initializer
// Payment constructor body
// CardPayment field initializer
// CardPayment constructor body
The CardPayment field initializer runs after the whole Payment constructor, not before it. javap -c CardPayment shows why: the compiler has no separate "field initialisation" step. It copies each field initializer into every constructor that calls super(...), right after that call:
CardPayment();
0: aload_0
1: invokespecial #1 // Method Payment."<init>":()V ← super() first
4: aload_0
5: ldc #7 // String CardPayment field initializer
7: invokestatic #9 // Method log:(Ljava/lang/String;)J
10: putfield #15 // Field surcharge:J ← then field initializers
13: ldc #19 // String CardPayment constructor body
15: invokestatic #9 // Method log:(Ljava/lang/String;)J ← then the body
18: pop2
19: return
Notice three things:
- A constructor is a method named
<init>in bytecode, andsuper()is aninvokespecialof the parent's<init>on the samethis(aload_0). No second object is created. - Field initializers and instance initializer blocks are inlined, in source order, between the
super(...)call and the body. Constructors that start withthis(...)skip them, because the constructor they delegate to already runs them. - The JVM verifier enforces the rule too: until the parent's
<init>returns,thisis "uninitialized". The only thing allowed is storing into the class's own fields, which javac uses for one hidden purpose: an inner class's reference to its enclosing instance.
The implicit super() and the compile error it causes
If a constructor doesn't start with this(...) or super(...), the compiler inserts super();. If a class declares no constructor at all, the compiler generates a default one whose body is super();. Both are fine until the parent has no no-arg constructor:
class Account {
private final String id;
Account(String id) { this.id = id; } // the only constructor
}
class SavingsAccount extends Account {
SavingsAccount() { } // compiler inserts super(); here
}
// error: constructor Account in class Account cannot be applied to given types;
// required: String
// found: no arguments
class SavingsAccount extends Account {} with no constructor at all gives the same error, which confuses people because the line it points at has no constructor in it. The fix is to call the right parent constructor explicitly, SavingsAccount(String id) { super(id); }, or to add a no-arg constructor to Account if a default really makes sense (it usually doesn't for an ID).
The rules for the call itself, in Java 21:
super(...)orthis(...)must be the first statement, and you can have only one of them.- Arguments can't refer to
this, instance fields or instance methods ("cannot reference this before supertype constructor has been called"). Static helpers are fine:super(normalise(id)). - Java 25 finalised flexible constructor bodies (JEP 513, previewed from Java 22 as JEP 447): you may run statements such as argument validation before
super(...), as long as they don't readthis. On Java 21 you put that validation in a static helper.
super.method(): the parent's implementation, without dispatch
Try it yourself first
CardPayment overrides both methods and calls super in each. What does new CardPayment().summary() return? Watch which fee() the parent's summary() calls.
class Payment {
long fee() { return 0; }
String summary() { return "fee=" + fee(); }
}
class CardPayment extends Payment {
@Override long fee() { return super.fee() + 150; }
@Override String summary() { return "[card] " + super.summary(); }
}
System.out.println(new CardPayment().summary()); // prints: [card] fee=150
super.summary() jumps straight into Payment.summary(). But inside it, fee() is an ordinary virtual call on this, and this is still the CardPayment object, so it runs CardPayment.fee(), which in turn calls super.fee() and adds 150. The figure below puts the two kinds of call next to each other.
Two calls compared: this.fee() compiles to invokevirtual and is looked up in the runtime class's vtable, while super.fee() compiles to invokespecial and is bound to Payment.fee at compile time
That's the key mental model: super.m() is statically bound, but it doesn't change who this is. In bytecode, super.fee() is invokespecial Payment.fee:()J, which the JVM resolves to the implementation in Payment (or the nearest ancestor that has one) without looking at the object's runtime class. A further subclass PremiumCardPayment that overrides fee() doesn't affect what CardPayment's super.fee() calls.
Two consequences come up in interviews. There is no super.super.fee(): you can only reach your direct parent's view, which protects the parent's encapsulation (if Payment wants its grandparent's behaviour bypassed, that's its decision, not yours). And super alone isn't a value: you can't return super; or pass super as an argument. super means "this object, but look up members starting from my parent".
super.field: reaching a hidden field
If a subclass declares a field with the same name as a parent field, it hides the parent's field, and super.channel is how you reach the parent's copy. Since super.channel is resolved by the compiler, the bytecode is a plain getfield Payment.channel. It works, but I treat it as a code smell: fields should be private, and a hidden field is almost always an accident. The inheritance lesson shows why fields are never polymorphic.
Interface.super.method(): choosing between defaults
When a class implements two interfaces whose default methods clash, the compiler forces an override. Inside it, InterfaceName.super.method() calls one specific default:
interface Refundable { default String policy() { return "refund within 7 days"; } }
interface Auditable { default String policy() { return "keep records 8 years"; } }
class CardPayment implements Refundable, Auditable {
@Override
public String policy() {
return Refundable.super.policy() + "; " + Auditable.super.policy();
}
}
System.out.println(new CardPayment().policy()); // prints: refund within 7 days; keep records 8 years
This also compiles to invokespecial, against an interface method reference. The named interface must be a direct superinterface of the class (or of the interface, when you write this inside another interface's default method). You can't skip a level and name a grandparent interface, for the same reason there's no super.super.
The diagram below shows the default-method tie break as a dispatch choice the class must make explicitly.
A class resolving two interface default methods must override the method and can call each direct default with InterfaceName.super.method
Where super isn't allowed
super always means "the current instance, viewed as its parent", so there must be a current instance:
class CardPayment extends Payment {
static void reconcile() {
super.fee(); // error: non-static variable super cannot be referenced from a static context
}
}
The same applies to static initializer blocks and static nested classes reaching their outer class. Records and enums can't call super(...) explicitly either: a record's canonical constructor implicitly chains to Record's, and an enum constructor implicitly chains to Enum's.
03Real world: the exception that lost its root cause
A classic production debugging dead end: a service wraps a failure in a custom PaymentDeclinedException, and the logs show only the wrapper, with no "Caused by:" section. The constructor accepted the cause but never passed it to super, so Throwable never recorded it. When the gateway timed out at 3 a.m., the on-call engineer saw "Payment declined" and nothing about the SocketTimeoutException underneath.
class PaymentDeclinedException extends RuntimeException {
// ❌ cause is accepted and dropped: stack trace shows no "Caused by:"
PaymentDeclinedException(String message, Throwable cause) { super(message); }
}
class PaymentDeclinedException2 extends RuntimeException {
// ✅ Throwable stores the cause and prints it in the stack trace
PaymentDeclinedException2(String message, Throwable cause) { super(message, cause); }
}
The same pattern shows up wherever a framework expects you to chain to the parent. Android's Activity.onCreate override must call super.onCreate(...), and the framework throws SuperNotCalledException if you forget. A servlet that overrides init(ServletConfig) without calling super.init(config) leaves getServletConfig() returning null. When you override a framework method, check its documentation for "subclasses must call super" before deciding to skip it.
04Advanced: construction order traps and the bytecode details
Calling an overridable method from a constructor. The parent constructor runs before the subclass's field initializers, so an override called from it sees default values:
class Payment {
Payment() { System.out.println("Created: " + describe()); }
String describe() { return "payment"; }
}
class CardPayment extends Payment {
private final String network = "VISA"; // a constant variable: inlined by javac
private final String last4;
private int retries = 3;
CardPayment(String last4) { this.last4 = last4; }
@Override String describe() { return network + " *" + last4 + " retries=" + retries; }
}
new CardPayment("4242"); // prints: Created: VISA *null retries=0
last4 is null and retries is 0, because neither has been assigned yet. network already looks correct only because a final field initialised with a compile-time constant is a constant variable, and javac inlines "VISA" at every use. Change it to = String.valueOf("VISA") and it prints null too. The rule from Effective Java (Item 19) is simple: constructors must not call methods that subclasses can override. Call private, static or final methods instead.
Many candidates say that super(...) "creates the parent object first, then the child object". There's only one object, allocated by the new instruction with room for every field in the hierarchy (all zeroed). super(...) runs the parent's initialisation code on that same object. This matters for the trap above: the subclass's override can run during the parent constructor precisely because the object is already a CardPayment from the moment it's allocated.
invokespecial covers three cases. The JVM uses it for constructors (<init>), super.m() calls and, before Java 11, calls to private methods. Since Java 11 (nestmates, JEP 181), javac emits invokevirtual for private method calls too, but the JVM still binds them directly because a private method can't be overridden. The common thread: invokespecial means "call exactly this implementation, don't search the runtime class".
super in inner classes. An inner class can call its enclosing class's parent method with Outer.super.method(). It's rare in application code, but if you see it in an interview snippet, read it as "the outer instance, viewed as its parent".
super(...) chains and super.m() calls cost effectively nothing after JIT compilation. invokespecial targets a single known method, so it's always inlinable, and Object.<init> is empty. A typical allocation in HotSpot is a thread-local (TLAB) pointer bump of a few nanoseconds, and a three-level constructor chain inlines into that same compiled code. Don't flatten a hierarchy for speed; flatten it if the design is wrong.
this |
super |
|
|---|---|---|
| Refers to | the current object | the current object, looked up from the parent class |
| Constructor call | this(...): another constructor of this class |
super(...): a constructor of the direct parent |
| Method call | this.fee(): invokevirtual, runtime class decides |
super.fee(): invokespecial, parent's implementation |
| Field access | own field (or inherited one) | the parent's field, even if hidden |
| Usable as a value | yes (return this;) |
no |
| Static context | not allowed | not allowed |
05How to say it in the interview
When I ask about super, I'm listening for construction order and dispatch. A good answer says that super(...) must come first (and that Java 25 relaxed this), explains the compile error from the implicit super(), and knows that super.m() is a non-virtual call while this inside the parent's method is still the subclass. Bonus points for invokespecial and for spotting the "overridable method in a constructor" trap.
In the interview
"super has three uses. super(...) chains to a parent constructor; in Java 21 it must be the first statement, and if I omit it the compiler inserts super(), which won't compile if the parent has no no-arg constructor. Construction is one object: new allocates and zeroes all fields, then each constructor runs top-down, and javac inlines field initializers right after the super call. super.m() calls the parent's implementation; it compiles to invokespecial, so there's no virtual lookup, but this inside that method is still the subclass, so calls it makes are dispatched normally. super.field reads a hidden field, and Interface.super.m() picks a default when two interfaces clash. There's no super.super, and no super in static code. The trap I watch for is calling an overridable method from a constructor, because the override sees uninitialised fields."
On the whiteboard
super(...) first statement (Java 21); one of this(...)/super(...)
omitted → compiler inserts super() → parent needs no-arg ctor
order new (alloc + zero) → Object.<init> → P init → C init
each <init>: super() → field inits → ctor body
super.m() invokespecial P.m (not virtual)
but `this` inside P.m is still C → its calls dispatch
super.f parent's hidden field (getfield P.f)
I.super.m() pick a default; I must be a direct superinterface
not allowed super.super, static context, `return super`
trap overridable call in ctor → sees null / 0
Java 25 JEP 513: statements before super() (no `this`)
Further reading (8)
- JLS §8.8.7 Constructor Body (explicit constructor invocations)
- JLS §12.5 Creation of New Class Instances
- JLS §15.12.1 Compile-Time Step 1 (super.m and TypeName.super.m)
- JVMS §6.5 invokespecial
- JEP 513: Flexible Constructor Bodies (Java 25)
- The Java Tutorials: Using the Keyword super
- Effective Java, 3rd ed., Item 19: Design and document for inheritance or else prohibit it
- Baeldung: Guide to the super Java Keyword