Inheritance and Polymorphism in Java: extends, super, and Dynamic Dispatch
Polymorphism is the payoff of object-oriented programming: you write code against a general type, and it automatically does the right thing for every specific type — including ones written years after your code shipped. Inheritance is one mechanism that enables it (interfaces are the other).
This guide covers extends, overriding, super, dynamic dispatch, casting with modern instanceof, and — importantly — when not to use inheritance, because overusing it is the classic intermediate-developer mistake.
Inheritance: is-a Relationships
extends says a class is a specialization of another:
public class Notification { protected final String recipient;
public Notification(String recipient) { this.recipient = recipient; }
public void send(String message) { System.out.println("Sending to " + recipient + ": " + message); }}
public class EmailNotification extends Notification { private final String subject;
public EmailNotification(String recipient, String subject) { super(recipient); // must call parent constructor first this.subject = subject; }
@Override public void send(String message) { System.out.println("Email to " + recipient + " [" + subject + "]: " + message); }}EmailNotification inherits everything Notification has, then overrides send with its own behavior. Key mechanics:
super(recipient)invokes the parent constructor and must be the first statement — a child can’t exist half-built on top of an unbuilt parent. If the parent has no no-arg constructor, the call is mandatory, and forgetting it is a common compile error.protectedmeans “visible to subclasses (and the package), hidden from everyone else” — a middle ground betweenprivateandpublic. Use sparingly; everyprotectedfield is API you now owe compatibility to.@Overridemakes the compiler verify a genuine override. A signature typo without it silently creates an unrelated method — one of Java’s sneakiest bug classes.- Java has single inheritance: one parent per class. Multiple interfaces are allowed — that’s next chapter’s topic and Java’s answer to the diamond problem.
Polymorphism: One Reference Type, Many Behaviors
Here’s the feature everything above exists to enable:
List<Notification> queue = List.of( new EmailNotification("a@x.com", "Invoice"), new SmsNotification("+91-98xxx"), new Notification("fallback@x.com"));
for (Notification n : queue) { n.send("Your order shipped"); // each object runs ITS OWN send()}The loop variable is typed Notification, but each call executes the actual object’s override. This is dynamic dispatch: the method chosen at runtime by the object’s real class, not by the variable’s declared type.
The two-type mental model is worth making explicit:
Notification n = new EmailNotification(...);─────┬────── ────────┬─────────── static type runtime type (what the compiler (what actually executes lets you CALL) when you call it)- The static type (
Notification) governs which methods you may call —n.getSubject()won’t compile even though the object has it. - The runtime type (
EmailNotification) governs what those calls do.
Why this matters practically: the notification-dispatch loop above never changes when you add PushNotification next quarter. New behavior arrives by adding a class, not by editing every if/else chain that handles notifications. That’s the open/closed principle in one loop — and it’s the difference between codebases that grow and codebases that rot.
One boundary: static methods and fields don’t dispatch dynamically. Only instance methods do. Accessing a field through a parent-typed reference gets the parent’s field; “overriding” a static method is actually hiding it. If you find yourself relying on either, restructure.
super Beyond Constructors: Extending, Not Replacing
An override can invoke the parent’s version — the “do the standard thing, plus” pattern:
@Overridepublic void send(String message) { audit.log("email-attempt", recipient); super.send(message); // parent's logic}Common for decorating behavior with logging, metrics, or validation. If you find every subclass calling super.x() in the same position, though, consider the template method pattern instead — the parent owns the skeleton and subclasses fill in one hook:
public abstract class Report { public final String generate() { // final: skeleton is fixed return header() + body() + "\n-- end --"; } protected String header() { return "== Report ==\n"; } protected abstract String body(); // each subclass supplies only this}final on a method means “cannot be overridden” (on a class: “cannot be extended” — String is final, which is partly how its immutability stays trustworthy).
Casting and Modern instanceof
Sometimes you hold a parent-typed reference and need child-specific API. The old idiom checked-then-cast in two steps; pattern matching for instanceof (standard since Java 16) fuses them:
// oldif (n instanceof EmailNotification) { EmailNotification e = (EmailNotification) n; System.out.println(e.getSubject());}
// modern — test, cast, and bind in one stepif (n instanceof EmailNotification e) { System.out.println(e.getSubject());}An unguarded wrong cast compiles fine and throws ClassCastException at runtime — the compiler can’t know a Notification variable isn’t holding an SMS. So casts always travel with an instanceof guard.
But pause on a design smell: a chain of instanceof checks over your own class hierarchy usually means a method is missing. If you’re writing
if (n instanceof EmailNotification e) { /* email logic */ }else if (n instanceof SmsNotification s) { /* sms logic */ }that per-type logic probably belongs inside the classes as an overridden method — that’s literally what dynamic dispatch is for. The legitimate exceptions: handling types you don’t own, and exhaustive switch over a sealed hierarchy (Java 21 pattern matching for switch), where the compiler verifies you’ve covered every permitted subtype:
String route = switch (n) { case EmailNotification e -> "smtp:" + e.getSubject(); case SmsNotification s -> "sms-gateway"; case Notification other -> "default";};The Fragile Base Class Problem — and Composition
Inheritance is powerful and dangerously easy to overuse. The classic cautionary tale, adapted from Effective Java:
public class CountingSet<E> extends HashSet<E> { private int addCount = 0;
@Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); // BUG: HashSet.addAll calls add()... }}Add three elements via addAll and addCount becomes 6, because the parent’s addAll internally calls add — which dispatches to your override, double-counting. Your subclass silently depends on the parent’s private implementation choices, which can also change in any JDK update.
The robust alternative is composition — hold the set, don’t be the set:
public class CountingSet<E> { private final Set<E> inner = new HashSet<>(); private int addCount = 0;
public boolean add(E e) { addCount++; return inner.add(e); } public boolean addAll(Collection<? extends E> c) { addCount += c.size(); boolean changed = false; for (E e : c) changed |= inner.add(e); // our add? our choice. return changed; } public int getAddCount() { return addCount; }}No hidden coupling: the inner HashSet can’t call back into your methods. The guideline that has survived twenty years of Java practice:
Inherit only when the subclass truly is-a parent and you control (or the author designed for) extension. Otherwise, compose.
Quick test before you write extends: would it be wrong for your class to expose every public method of the parent? A Stack extends Vector (a real JDK mistake — you can insertElementAt into the middle of a JDK stack) fails this test. When in doubt, composition — you can always refactor toward inheritance; the reverse breaks clients.
Abstract Classes: Partial Blueprints
When a base type should never be instantiated directly, mark it abstract:
public abstract class PaymentMethod { protected final String accountId;
protected PaymentMethod(String accountId) { this.accountId = accountId; }
public abstract Receipt charge(long amountPaise); // no body — subclasses must implement
public boolean supportsRefund() { return true; } // shared default behavior}new PaymentMethod(...) won’t compile; new UpiPayment(...) will, provided it implements charge. Abstract classes shine when subclasses share state and partial implementation. When you only need to promise capability with no shared state, an interface is lighter — the full comparison is next in Interfaces and Abstract Classes.
Overriding Rules Worth Knowing Cold
A valid override must keep the same signature, and:
- Visibility can widen, never narrow — a
publicmethod can’t becomeprotectedin a child (callers through the parent type would break). - Return type may narrow (covariant returns): parent returns
Notification, child may returnEmailNotification. - Checked exceptions may narrow or vanish, never broaden — the child can’t surprise parent-typed callers with new checked exceptions.
equals/hashCode/toStringare overrides too — same@Overridediscipline applies.
All three rules are one principle wearing different hats: anywhere a parent is expected, a child must be substitutable (Liskov substitution). A subclass that throws UnsupportedOperationException from an inherited method, or needs stricter preconditions, is telling you the is-a relationship was a lie.
Polymorphism in the Wild: Three Patterns You’ll Meet This Year
Theory lands better with the shapes it takes in production codebases.
Strategy objects. A checkout that prices differently per customer tier doesn’t if/else on tier — it holds a PricingStrategy and calls strategy.price(cart). Each tier is a class (or, for single-method strategies, a lambda); swapping behavior means swapping the object. Test code injects a trivial strategy; production wiring injects the real ones. Once you see this shape, you’ll notice half of good OO design is it.
Template methods in frameworks. When you extend a framework base class and override one or two hooks (configure(), doFilter(), onMessage()), you’re living inside the template pattern from this guide — the framework owns final skeleton methods and dispatches to your overrides at the right moments. Understanding that inversion (“don’t call us, we’ll call you”) is what makes framework documentation suddenly readable.
Test doubles. Every mock and fake works because of substitutability: the code under test declares a dependency by its supertype, and the test hands in a subclass (or interface implementation) that records calls or returns canned data. Codebases that are “hard to test” are, almost always, codebases that depend on concrete classes — polymorphism is the crowbar that opens them.
Frequently Asked Questions
Can constructors be overridden? No — constructors aren’t inherited at all. Each class defines its own and chains to the parent via super(...). Related trap: calling an overridable method from a constructor is dangerous, because the child’s override runs before the child’s fields are initialized. Keep constructors to field assignment and validation; call virtual methods after construction.
What does Object give every class? equals, hashCode, toString (the trio you routinely override), plus getClass, and the low-level wait/notify machinery you’ll likely never touch directly. Every reference type is substitutable where Object is expected — which is what pre-generics collections relied on, and why generics were such an upgrade.
Why can’t Java classes extend two parents? The diamond problem: two parents providing different implementations (and worse, different state) for the same inherited member, with no principled way to choose. Interfaces sidestep it — no state, and default-method conflicts must be resolved explicitly by the implementing class.
Is instanceof always a smell? No — the smell is replacing polymorphism with it on hierarchies you control. It’s legitimate at system boundaries (deserialized objects, third-party types), in equals implementations, and in exhaustive pattern-matching switches over sealed types, where the compiler turns it from a hazard into a checked construct.
Self-Check
Notification n = new EmailNotification("a@x", "Hi");— doesn.send(...)run the parent or child version? Can you calln.getSubject()?- Why must
super(...)be the first statement in a child constructor? CountingSetdouble-counted with inheritance. In one sentence, why did composition fix it?- You need a new
WhatsAppNotification. Which existing code must change in the dispatch loop from the polymorphism section?
(Answers: child’s, via dynamic dispatch; no — the static type gates callable methods. The parent’s invariants must be established before child construction proceeds. The inner set can no longer invoke the wrapper’s methods, so there’s no self-call coupling. None — that’s the point.)
Next: Interfaces and Abstract Classes — Java’s contract mechanism, default methods, and how to choose between the two abstraction tools.