Java

Inheritance and Polymorphism in Java

Executive Summary

With inheritance and polymorphism in Java, a subclass declared with extends inherits the parent’s visible fields and methods, adds its own, and can override methods to change behavior, calling super when it needs the parent’s version. Polymorphism means the method that runs is chosen by the object’s runtime type, not the variable’s declared type: a PaymentMethod variable holding a CardPayment runs CardPayment’s methods. Java allows only single class inheritance; interfaces, covered next in the comparison article, provide multiple contracts. Overriding must match the parent’s signature exactly, cannot reduce visibility, and must honor the substitution principle: a subclass must be usable wherever the parent is expected. Use inheritance for genuine is-a relationships with real behavior to share, and prefer composition otherwise, because inheritance is the strongest coupling in the language.

Inheritance in Java: extends and What a Subclass Gets

Inheritance models the is-a relationship: a CardPayment is a PaymentMethod, so it deserves everything a PaymentMethod has. The extends keyword declares that, as the subclasses lesson defines, and one rule keeps hierarchies shallow: a class extends exactly one parent. Here is the full working example this article builds on:

class PaymentMethod {
    protected final String owner;          // protected: for subclasses

    PaymentMethod(String owner) {
        this.owner = owner;
    }

    public String describe() {
        return owner;
    }

    public long feeCents(long amountCents) {
        return 0;
    }
}

class CardPayment extends PaymentMethod {
    private final String last4;

    CardPayment(String owner, String last4) {
        super(owner);                      // parent constructor runs first
        this.last4 = last4;
    }

    @Override
    public String describe() {
        return super.describe() + " card *-" + last4;   // reuse, then extend
    }

    @Override
    public long feeCents(long amountCents) {
        return amountCents * 29 / 1000;    // 2.9 percent
    }
}

class BankTransfer extends PaymentMethod {
    private final String ibanTail;

    BankTransfer(String owner, String ibanTail) {
        super(owner);
        this.ibanTail = ibanTail;
    }

    @Override
    public String describe() {
        return super.describe() + " bank transfer **" + ibanTail;
    }

    @Override
    public long feeCents(long amountCents) {
        return 50;                          // flat fee, amount ignored
    }
}

public class Checkout {
    public static void main(String[] args) {
        PaymentMethod[] methods = {
            new CardPayment("Ada", "4242"),
            new BankTransfer("Linus", "91AB")
        };
        for (PaymentMethod method : methods) {
            System.out.println(method.describe()
                    + " fee on 100.00: " + method.feeCents(10_000));
        }
    }
}
Ada card *-4242 fee on 100.00: 290
Linus bank transfer **91AB fee on 100.00: 50

Three details in that code carry the whole mechanism. The subclass constructor calls super(…) first, because Java builds parent state before child state. The @Override annotation asks the compiler to verify you are genuinely overriding, and catches the signature typos that would otherwise silently create overloads. And protected on owner is the visibility decision from the encapsulation article: it exists precisely for subclasses like these.

Method Overriding and super

Overriding replaces an inherited method’s implementation while keeping its contract: same name, same parameters, same return type (or a subtype). The subclass version runs for subclass objects. Meanwhile, super.method() reaches the parent’s implementation when you want to reuse it, exactly as describe() does above. Overriding is not overloading, which the methods article defined as same name plus different parameters, resolved at compile time. In short, overriding is same signature, resolved at runtime.

Polymorphism: One Variable, Many Behaviors

Finally, the main method above is the payoff. The array is declared PaymentMethod[], but each element holds a different runtime type, and each call dispatches to that type’s version. That is dynamic dispatch, and the polymorphism lesson calls it the third pillar of object-oriented programming:

      PaymentMethod[] methods
        |                    |
        v                    v
   CardPayment         BankTransfer        (runtime types)
        |                    |
        v                    v
   feeCents()  290     feeCents()  50      (each type's version runs)

   static type:  PaymentMethod   (what the compiler checks)
   dynamic type: CardPayment     (what the JVM dispatches on)

The compiler checks that PaymentMethod has a feeCents method. However, the JVM picks CardPayment’s or BankTransfer’s version based on the object actually referenced. Consequently, adding a CryptoPayment subclass requires zero changes to Checkout: the loop already speaks PaymentMethod. In fact, that open-closed behavior is the reason polymorphism exists. For a compact overview of all four OOP pillars side by side, see Java OOP principles from the Java Mini Series.

The Rules of Overriding

The JLS class chapter defines the contract, and four rules cover everything you need daily:

  1. The name and parameter list must match the parent exactly; the return type must match or narrow (a subtype).
  2. Visibility cannot be reduced: public in the parent stays public in the child.
  3. final methods cannot be overridden, static methods are hidden rather than overridden, and private methods are not inherited at all.
  4. In other words, a subclass must remain usable wherever the parent is expected. That is the substitution principle, and it is a design rule more than a syntax rule.

The Overload-Instead-of-Override Trap

Wrong code first, showing the silent version of the trap:

// WRONG: one character of parameter type turns an override into an overload
class Parent {
    void send(String message) { }
}

class Child extends Parent {
    void send(Object message) { }   // overload: never runs through Parent
}

Parent p = new Child();
p.send("hello");    // runs Parent.send: dynamic dispatch never finds Child's
// RIGHT: match the signature, and let the compiler verify
class Child extends Parent {
    @Override
    void send(String message) { }   // @Override fails the build if you slip
}

The delta: dynamic dispatch only overrides exact signatures. The @Override annotation converts this silent bug into a compile error, which is why you should never write an override without it.

Field Hiding: Fields Do Not Dispatch

Only methods are polymorphic. Fields are resolved by the declared type, which surprises everyone once:

class Parent { String tag = "parent"; }
class Child extends Parent { String tag = "child"; }

Parent p = new Child();
System.out.println(p.tag);   // "parent": fields do not override

However, duplicating field names across a hierarchy is legal and almost always a mistake. So, keep fields private, and expose reads through methods, which do dispatch.

How Real Systems Do This

The JDK is built on inheritance where it earns its keep. InputStream and OutputStream form families (FileInputStream, BufferedInputStream) that differ in behavior while sharing the read/write contracts; Number parents Integer and Long; the exception hierarchy you will meet in Part 3 is a class tree. In each case the is-a relationship is genuine and the shared behavior is real.

In my experience, inheritance goes wrong through ambition rather than accident. The worst hierarchy I inherited had seven levels of BaseController ending in classes that overrode three quarters of the parent. In short, the is-a relationship died at level four. We replaced it with composition, one domain object and small collaborators. As a result, the bug count in that area halved in two months. Consequently, my personal bar: if a subclass overrides more than half its parent, it was never a subclass.

However, the modern direction is even stricter. Sealed hierarchies, covered in the sealed classes article, restrict who can extend a class so the compiler can verify exhaustive handling. And the abstract classes vs interfaces comparison, two articles ahead, settles when each tool is right.

Decision Framework

Ask these in order before you type extends.

  1. Is the is-a relationship real in the domain, not just convenient in the code? CardPayment is a PaymentMethod; Car is not an Engine.
  2. Will the subclass honor every parent contract, including the ones you have not written yet? If not, composition is calling.
  3. Is there meaningful behavior to share, not just two fields? Shared shape with no shared behavior wants a record or an interface.
  4. Do you need to substitute subclass objects into parent-typed code? That is the point of polymorphism; without it, inherit nothing.
  5. Should extension be open or closed? Open: normal classes. Closed and verified: sealed classes, or final to forbid extension entirely.

When NOT to Use This

  • Do not inherit to reuse one utility method. After all, inheritance couples subclass to parent forever; a static helper or a composed collaborator delivers the reuse without the chain.
  • Do not extend concrete classes you do not control, such as ArrayList, in production code. The parent can change under you across JDK versions; wrap and delegate instead.
  • Do not build hierarchies deeper than two or three levels. Every level adds a place for hidden state and overridden contracts, and readers pay that tax forever.

Common Mistakes

  • Overloading when you meant to override. A parameter type one step off creates a new method, and the parent’s version keeps running. @Override makes it a compile error.
  • Duplicating field names, then expecting p.tag to be polymorphic. Fields resolve by static type; only methods dispatch.
  • Reducing visibility in the override, for example public to package-private. It does not compile, and that error protects your callers.
  • Calling overridable methods from a constructor. The subclass override runs before subclass fields initialize, so it reads nulls. Make such methods final or private.
  • instanceof chains followed by casts, instead of polymorphism. Every new type reopens the chain; an override closes that path permanently.
  • Calling super everywhere out of habit. If every override starts with super.method(), question why you overrode at all; usually the design wants composition.

Key Takeaways

  • extends creates a single-parent is-a relationship: the subclass inherits visible fields and methods and can add or override.
  • Subclass constructors call super(…) first, because parent state builds before child state.
  • Overriding requires an exact signature match and cannot reduce visibility; always annotate with @Override.
  • Dynamic dispatch picks the method by runtime type, so PaymentMethod variables run CardPayment’s or BankTransfer’s code.
  • Fields do not dispatch; they resolve by declared type. Keep them private and read through methods.
  • Polymorphism removes instanceof chains: new subclasses work with zero changes to code that speaks the parent type.
  • Inheritance is the strongest coupling in Java; reserve it for real is-a relationships and prefer composition otherwise.

FAQ

What is the difference between inheritance and polymorphism in Java?

Inheritance is the mechanism: a subclass acquires a parent’s members and can override behavior. Polymorphism is the effect: one variable type can hold many runtime types, and each call runs the version the runtime object defines.

Can a Java class extend multiple classes?

No. Java allows single class inheritance to keep dispatch and construction unambiguous. A class can implement many interfaces, which the next articles cover, and composition supplies the reuse that multiple inheritance would offer elsewhere.

What is the difference between overriding and overloading?

Overriding reuses the exact signature in a subclass and resolves at runtime. Overloading shares a name with different parameter lists in the same class and resolves at compile time. A near-miss override becomes an overload silently, which @Override prevents.

What does super do in Java?

Two things: super(…) calls the parent constructor as the first statement of a subclass constructor, and super.method() calls the parent’s implementation of an overridden method from within the child’s version.

What is the substitution principle?

The rule that a subclass must be usable everywhere its parent is expected, without surprises. If every CardPayment breaks some PaymentMethod contract, the hierarchy is a lie, and callers discover it in production.

Conclusion

Inheritance gives Java its substitutability: one declared type, many runtime behaviors, and dispatch that keeps new subclasses working with old code. The costs are real and permanent, so the discipline matters: true is-a relationships, shallow trees, @Override on every method, and composition when the relationship is convenience rather than identity.

The next article descends to the root of every hierarchy: java.lang.Object, the silent parent of all your classes, and the equals, hashCode, and toString contracts you inherit whether you thought about them or not.

Inherit identity, compose everything else. That sentence is thirty years of hard-won Java design advice.

Last updated on 26 September 2026.

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *