Back to Blog
Java

How to Use Java Super Keyword Correctly

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

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. The super.validate() call is a regular method invocation; it obeys normal dynamic dispatch rules, but it explicitly selects the parent's implementation.

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, you have no direct syntax for it. The typical workaround is to add a protected bridge method in the parent:

class Parent { void run() { System.out.println("Parent"); } } class Grandchild extends Parent { @Override void run() { // cannot call super.super.run() System.out.println("Grandchild"); } }

You would have to introduce a method in Parent that invokes the desired grandparent behavior, then call that from Grandchild.

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 java super keyword itself has no measurable runtime cost. It is a compile-time construct that generates the same bytecode as a normal method call or field access, with the target 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 reference to the parent-typed view of the current object, cast this to the parent type:

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

Casting does not change which method implementation runs; it only changes the compile-time type. 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.

When Not to Use super() for constructor calls

In some cases you may want to avoid calling a parent constructor that does side effects. For example, if the parent constructor logs or sends an event, every subclass instantiation triggers that behavior. You cannot prevent the parent constructor from running, but you can choose to call the least surprising constructor. There is no way to bypass the parent constructor entirely. This is by design in Java: object initialization is always bottom-up, from the Object class 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.

Using super in Records and Sealed Classes

Java 16 introduced records, and Java 17 sealed classes. A record can extend another record only through its canonical constructor, and that constructor must call super(...). The records themselves are implicitly final, so you cannot create further subclasses. Sealed classes restrict which classes can extend them. When you write a subclass of a sealed class, the super keyword works exactly as described, but the compiler enforces that the parent class explicitly permits the subclass using permits. This does not change the semantics of super; it only changes which classes may appear in the hierarchy.

The key point is that super is unaffected by these features. The syntax and resolution rules remain the same. The only difference is that you will more likely encounter compact constructor forms in records, where the implicit call to super happens before any field assignments you write.

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