Back to Blog
Java

How to Use Java Super Keyword Correctly

Learn when and where to use the Java super keyword: constructor chaining, invoking parent methods, accessing shadowed fields, and avoiding common mistakes.

super keywordJava inheritanceconstructor chainingmethod overridingJava Object class
Diagram showing a subclass arrow pointing to a parent class, with labels for super constructor and super method call in Java

When a subclass needs to call a parent constructor, run an overridden method, or read a field that its own member hides, the Java super keyword is the tool. It is not a general reference to the parent instance; it is a contextual reference that only works inside an instance method, constructor, or initializer block of a subclass. Understanding exactly where it can appear and what it resolves to prevents a class of bugs that appear when class hierarchies grow beyond two levels.

What the super Keyword Actually Resolves To

In Java, super does not behave like a variable. You cannot pass it around, store it in a field, or compare it. It compiles to a reference that lets you reach members of the immediate parent class that would otherwise be hidden or overridden.

Consider this minimal pair:

class Parent { String name = "parent"; void print() { System.out.println("Parent::print"); } } class Child extends Parent { String name = "child"; void print() { System.out.println("Child::print"); } void showParent() { System.out.println(super.name); // resolves to Parent.name super.print(); // resolves to Parent.print() } }

Every use of super in Child refers to the Parent class directly above it. If Parent itself extends Grandparent, super does not automatically skip to the grandparent; you can only reach the immediate parent through super. To call a method inherited from Grandparent but overridden in Parent, you would need a reference that is not available through super alone.

This immediate-parent rule is the most common source of confusion when using the keyword in deep hierarchies.

Using super() to Chain Constructors

The most frequent use of super is the zero-argument or parameterized constructor call. The call must be the first statement in a constructor:

class Parent { private final String config; Parent(String config) { this.config = config; } } class Child extends Parent { private final int retries; Child(String config, int retries) { super(config); // must be first this.retries = retries; } }

If you omit an explicit super(...) call, the compiler inserts a call to the no-argument constructor of the parent. If the parent does not declare a no-argument constructor, the code will not compile. That behavior is deterministic and often the first hint that a parent needs a parameterized constructor.

A common design pattern is to have one constructor perform the full initialization and other constructors delegate to it with this(...), while the primary constructor uses super(...):

class Child extends Parent { Child(String config) { this(config, 3); } Child(String config, int retries) { super(config); this.retries = retries; } }

Note that you cannot call both this(...) and super(...) in the same constructor. The first statement can only be one of them.

Accessing Parent Fields When Names Collide

Field shadowing happens when a subclass declares a field with the same name as a field in the parent. The super keyword lets you reach the parent's field explicitly.

class Counter { int count = 0; } class ExtendedCounter extends Counter { int count = 10; void resetToParentDefault() { count = super.count; // assigns the parent's value (0) } }

Field shadowing is legal but is widely considered poor design because it makes the object's state harder to follow. In most cases, a better choice is to make the parent field private and expose it with a getter and setter, or avoid reusing the same name altogether. When you do need to shadow, super provides the only clean way to read the parent's value.

The same applies inside a constructor: if a parameter name matches a field, this.field refers to the current class member, and super.field refers to the parent's member.

Overriding Methods and Calling the Parent Implementation

A very common pattern is to extend an overridden method by first calling the parent version:

class BaseService { void validate() { System.out.println("Base validation"); } } class ExtendedService extends BaseService { @Override void validate() { super.validate(); // run the base logic first System.out.println("Extended validation"); } }

This is especially useful when the parent method performs essential setup that the subclass wants to preserve. Unlike a normal virtual call, super.validate() is compiled to an invocation that selects the parent's implementation directly instead of being dispatched to a further override.

One subtle point: super can only invoke an instance method of the immediate parent. You cannot use super.super.method() to skip a generation. If you need to call a grandparent method that was overridden in the parent, there is no direct syntax. A common workaround is to add a bridge method in the parent:

class Grandparent { void run() { System.out.println("Grandparent"); } } class Parent extends Grandparent { @Override void run() { System.out.println("Parent"); } protected void runGrandparent() { super.run(); // invokes Grandparent.run() } } class Grandchild extends Parent { @Override void run() { runGrandparent(); // no super.super.run() syntax } }

Here Parent.runGrandparent() exposes the grandparent implementation that Grandchild cannot reach directly with super.

Accessing the Parent's Constructor from a Nested Class

The super keyword also appears in inner classes that extend an enclosing class. If a static nested class extends its enclosing class directly, the nested class constructor can use super(...) in the usual way. For an inner (non-static) class that extends its enclosing class, the implicit enclosing instance is available, and super(...) still calls the parent constructor. The syntax does not change, but the compilation semantics differ because of the enclosing instance.

For example:

class Outer { class Inner extends Outer { Inner() { super(); // calls Outer() } } }

This pattern is rare and usually signals a design issue, but it clarifies that super always refers to the actual parent class, regardless of nesting.

Runtime Cost and Memory Implications

The super keyword itself has no direct runtime overhead. It is a compile-time construct that compiles to ordinary method-invocation or field-access bytecode, with the target member resolved to the parent class. No extra object is created, and no reflective lookup occurs.

The real cost appears in constructor chaining. Every object construction walks up the inheritance chain, and each constructor in the chain executes its own initialization. This is unavoidable, but it means that deep hierarchies invoke many constructor bodies, even if they are empty. If a parent constructor performs expensive setup, every subclass instantiation pays that cost. There is no way to skip a constructor in the chain; the compiler guarantees that a parent constructor runs. This is a design consideration rather than a performance problem caused by the keyword.

Common Misconceptions and Pitfalls

One frequent error is attempting to use super in a static context. Because a static method has no instance, the compiler rejects any reference to super or this in a static method. There is no syntax for calling a parent's static method through super; you would simply use the parent class name and the method directly.

Another misconception is that super gives you a reference to the parent object. It does not. You cannot assign super to a variable, pass it as an argument, or return it. If you need a parent-typed reference to the current object, assign this to a variable of the parent type:

Parent p = this; // okay p.print(); // dynamic dispatch still calls Child's print()

Changing the static type does not change which method implementation runs; it only affects compile-time method selection. To call the parent's version, you must use super.print() directly.

A third pitfall is relying on the implicit super constructor call when the parent only has a parameterized constructor. The compiler will complain. The fix is to add a no-argument constructor in the parent or to call an existing parameterized constructor explicitly.

Constructor Calls Are Not Optional

You may wish to avoid a parent constructor that has side effects, such as logging or sending an event, because every subclass instantiation triggers them. You cannot prevent the parent constructor from running, but you can choose the least surprising existing constructor. There is no way to bypass the parent constructor entirely. This is by design in Java: object initialization runs from Object down to the current class.

If you find yourself fighting the constructor chain repeatedly, reconsider the inheritance design. Prefer composition over inheritance when the subclass only needs a few methods from the parent and the constructor chain creates heavy dependencies. The super keyword remains the correct tool for legitimate inheritance, but it is not a workaround for a hierarchy that should not exist.

Records, Sealed Classes, and super

Records became a standard feature in Java 16, and sealed classes in Java 17. Records are implicitly final and cannot explicitly extend any class, so they cannot extend or be extended by another record. Every record's superclass is java.lang.Record; the super keyword therefore has no special inheritance role in records.

Sealed classes restrict which classes may extend them. When you write a permitted subclass, the compiler checks that the subclass is listed in the parent's permits clause (or is declared in the same source file, in which case the clause may be omitted). The super keyword works exactly as described; the feature changes which classes may appear in the hierarchy, not how super resolves.

Final Code Example: Combining super for Constructor and Method Calls

class Vehicle { private final String model; Vehicle(String model) { this.model = model; } void start() { System.out.println("Starting " + model); } } class Car extends Vehicle { private final int doors; Car(String model, int doors) { super(model); this.doors = doors; } @Override void start() { super.start(); System.out.println("Unlocking " + doors + " doors"); } }

The super constructor call initializes the parent's model field, and the super.start() call runs the parent's original starting sequence before the car-specific logic. This combination is the standard way to extend behavior without duplicating parent code. Notice that the parent field model is private, so the subclass cannot access it directly; it relies on the parent's methods. That separation keeps the internal state well encapsulated while still allowing the subclass to extend behavior cleanly.

Understanding these boundaries keeps your code predictable: super gives you controlled access to the immediate parent, not a way to reach arbitrary ancestors or to bypass encapsulation. Used correctly, it produces readable inheritance chains that are easy to maintain.

Java super keyword: Practical Usage and Code Examples | RYUSLOG DEV