Technology  /  Java

Java 15 guides · updated 2026

From the JVM to streams, generics, and concurrency — the language behind most enterprise backends, taught with modern Java in mind.

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:


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)

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:

@Override
public 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:

// old
if (n instanceof EmailNotification) {
EmailNotification e = (EmailNotification) n;
System.out.println(e.getSubject());
}
// modern — test, cast, and bind in one step
if (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:

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

  1. Notification n = new EmailNotification("a@x", "Hi"); — does n.send(...) run the parent or child version? Can you call n.getSubject()?
  2. Why must super(...) be the first statement in a child constructor?
  3. CountingSet double-counted with inheritance. In one sentence, why did composition fix it?
  4. 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.