Engineering Notes
Home/Languages/Java/Inheritance and Polymorphism
Languages · Java

Inheritance and Polymorphism

A class can extend exactly one superclass (Java has no multiple class inheritance, unlike C++). Every class that doesn't explicitly extend anything implicitly extends Object.

💻 Languages● Beginner⏱ 8 min read🛠️ 3 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • extends and Single Inheritance
  • super — Calling Up the Chain
  • Method Overriding Rules
  • Fields Are Not Polymorphic — Only Methods Are
  • Abstract Classes

1extends and Single Inheritance

A class can extend exactly one superclass (Java has no multiple class inheritance, unlike C++). Every class that doesn't explicitly extend anything implicitly extends Object.

java
class Animal {
    String name;
    Animal(String name) { this.name = name; }
    String speak() { return name + " makes a sound"; }
}

class Dog extends Animal {
    Dog(String name) { super(name); } // must call a superclass constructor first
    @Override
    String speak() { return name + " barks"; }
}

Animal a = new Dog("Rex");
System.out.println(a.speak()); // "Rex barks" — the OVERRIDE runs, even though `a` is typed Animal

2super — Calling Up the Chain

super(...) calls a superclass constructor and must be the first statement in a subclass constructor (if you don't write it, the compiler inserts an implicit super() — the no-arg version — which fails to compile if the superclass has no no-arg constructor). super.method() calls the superclass's version of an overridden method explicitly.

java
class Cat extends Animal {
    Cat(String name) { super(name); }
    @Override
    String speak() {
        return super.speak() + ", specifically a meow"; // extends rather than replaces the parent behavior
    }
}

3Method Overriding Rules

An override must have the same signature (name + parameter types), a covariant or identical return type, and cannot narrow the declared checked exceptions or reduce visibility. @Override is optional but strongly recommended — it makes the compiler verify you actually matched a superclass method, catching typos that would otherwise silently create an unrelated overload instead of an override.

java
class Shape {
    Shape self() { return this; }
    void resize(int factor) throws Exception { }
}

class Circle extends Shape {
    @Override
    Circle self() { return this; }         // covariant return type: Circle is-a Shape — allowed
    @Override
    void resize(int factor) { }              // narrowing (dropping) a checked exception is allowed
    // void resize(int factor) throws IOException {} // ERROR if not declared by the superclass — cannot WIDEN
}

4Fields Are Not Polymorphic — Only Methods Are

This is one of the most important and most surprising rules in the language: field access is resolved by the declared (compile-time) type of the reference, not the runtime type of the object. Overriding only applies to methods.

java
class Base { String label = "base"; }
class Derived extends Base { String label = "derived"; }

Base b = new Derived();
System.out.println(b.label);            // "base" — field access uses the DECLARED type
System.out.println(((Derived) b).label); // "derived" — only visible after an explicit cast

5Abstract Classes

An abstract class cannot be instantiated and may declare methods with no body (abstract methods) that subclasses are forced to implement.

java
abstract class Shape {
    abstract double area();                    // no body — subclasses MUST implement
    String describe() { return "area=" + area(); } // concrete method, can call the abstract one
}

class Square extends Shape {
    double side;
    Square(double side) { this.side = side; }
    @Override
    double area() { return side * side; }
}

6Interfaces

An interface defines a contract; a class can implement any number of interfaces (this is Java's answer to the lack of multiple class inheritance). Since Java 8, interfaces may also provide default method bodies and static methods.

java
interface Movable {
    void move(int dx, int dy);

    default String describeMovement() { // default method — concrete, inherited unless overridden
        return "can move";
    }
}

interface Drawable {
    void draw();
}

class Sprite implements Movable, Drawable {
    public void move(int dx, int dy) { /* ... */ }
    public void draw() { /* ... */ }
}
Abstract classInterface
Instance fieldsyesonly public static final constants
Constructorsyesno
Multiple inheritanceno (single extends)yes (multiple implements)
Method bodiesyes (mix of abstract + concrete)yes, via default/static (Java 8+)
When to useshared state + a strong "is-a" hierarchya capability/contract many unrelated classes can satisfy

7Object's Methods: equals, hashCode, toString

Every class inherits equals, hashCode, and toString from Object. The defaults are reference-identity based and rarely what you want for value-like classes.

java
class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false; // pattern-matching instanceof, Java 16+
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }
}
Animal.speak and Dog.speak both occupy slot 3 of their respective method tables; invokevirtual resolves through the runtime object's actual table, landing on Dog's override even when the reference is declared as Animal
Animal.speak and Dog.speak both occupy slot 3 of their respective method tables; invokevirtual resolves through the runtime object's actual table, landing on Dog's override even when the reference is declared as Animal
Under the HoodHow it works internally▾

Dynamic dispatch is table-driven, not a linear name search at call time. Every class HotSpot loads has an associated virtual method table (informally, a "vtable," though HotSpot's actual internal structures are more elaborate — Klass::vtable()): a flat array where each slot corresponds to one overridable method, and overriding a method in a subclass simply overwrites that same slot with a pointer to the subclass's implementation rather than adding a new slot. When you call a.speak() on a reference declared Animal, the compiler emits an invokevirtual instruction identifying which slot speak lives in (resolved once, at compile/link time, from the static type). At runtime, the JVM looks at the object's actual class pointer (from the object header, see the OOP Fundamentals topic), follows it to that class's vtable, and reads out whatever is currently in that slot — which is Dog's override if the object is really a Dog. The slot index is fixed by the static type hierarchy; only the contents of the slot vary by runtime type. This is why overriding doesn't require walking a name-based lookup chain at every call — it's an array index.

Interface method calls use a separate mechanism: itables (or a linear search fallback). Because a class can implement arbitrarily many unrelated interfaces, there's no single shared slot layout the way there is for a linear extends chain — so calling a method through an interface reference uses invokeinterface, historically resolved via a per-class itable (interface method table) or, in older/simpler JVM implementations, a linear scan of the class's interface method map. Modern HotSpot builds and caches itables per class specifically to avoid the slower linear search, and the JIT aggressively inlines and de-virtualizes these calls once it has observed the call site is monomorphic (sees only one concrete receiver type) — the same inline-caching idea used for ordinary virtual calls (see the Prototypes topic's JS equivalent — the underlying "monomorphic/polymorphic/megamorphic" inline cache classification is a shared JIT technique across V8 and HotSpot, not JavaScript-specific).

instanceof and casting are runtime checks against the class metadata, not the vtable. (Derived) obj compiles to a checkcast bytecode instruction that walks the object's actual class hierarchy metadata (not the vtable) to verify the cast is legal, throwing ClassCastException if not — this is a genuinely separate mechanism from method dispatch, even though both ultimately consult the object header's class pointer.

⚠️ Common Pitfalls

  1. Calling an overridable method from a constructor — the superclass constructor runs before subclass field initializers, so an overridden method invoked mid-construction can observe uninitialized subclass state (see the OOP Fundamentals topic's "Under the Hood" for exactly why: invokespecial for the constructor happens first, but the vtable slot for the overridden method already points at the subclass's version).
  2. Overriding equals() without hashCode() (or vice versa) — violates the contract that equal objects must have equal hash codes, silently breaking HashMap/HashSet lookups (see the Collections Framework topic) even though nothing throws an exception.
  3. Assuming fields are polymorphic like methods — base.label in section 4 resolves by declared type, not runtime type; this trips up anyone coming from a language where all member access is virtual.
  4. Accidentally overloading instead of overriding — a subtle signature mismatch (wrong parameter type, missing/extra parameter) creates a brand-new overload rather than an override, and without @Override the compiler won't warn you; the superclass version silently keeps running for callers using the superclass type.
  5. Widening a checked exception in an override — a subclass override may only throw the same, narrower, or no checked exceptions than the method it overrides — never a broader or unrelated checked exception — because callers coded against the superclass type never expect it.

📋 Quick Reference

java
class Dog extends Animal {                 // single inheritance only
    Dog(String name) { super(name); }      // super(...) must be first statement
    @Override String speak() { ... }        // @Override catches signature mismatches
}

interface Movable {                          // multiple interfaces allowed
    void move(int dx, int dy);
    default String info() { return "..."; } // Java 8+ default method
}

abstract class Shape {                       // cannot instantiate directly
    abstract double area();                  // subclass MUST implement
}

Base b = new Derived();
b.overriddenMethod();  // Derived's version  — methods: runtime type wins
b.field;                 // Base's version     — fields: declared type wins

// Object contract: override equals + hashCode together, always

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1Overloading vs overriding?core▾

Overloading: same method name, different parameters, resolved at compile time. Overriding: subclass redefines an inherited method with the same signature, resolved at runtime (dynamic dispatch). Use @Override.

Follow-ups interviewers ask
  • Can you override static methods? No, they're hidden, not overridden.
Q2How does dynamic dispatch work?core▾

The JVM picks the method implementation based on the object's runtime class, via virtual method tables, enabling polymorphism through base types and interfaces.

Follow-ups interviewers ask
  • Which methods aren't virtual? static, private, and final ones (statically bound).
Q3Why favour composition over inheritance?core▾

Inheritance tightly couples subclasses to parent implementation details (fragile base class problem); composition and interfaces allow swapping behaviour and testing more easily.

Follow-ups interviewers ask
  • Example? A Car has an Engine rather than extending Engine.
Q4What is the Liskov Substitution Principle in Java terms?senior▾

Subtypes must honour the base type's contract: same preconditions or weaker, postconditions or stronger, no unexpected exceptions. Violations show up as instanceof checks and UnsupportedOperationException.

Follow-ups interviewers ask
  • JDK example? Unmodifiable lists throwing on add.
Q5How does pattern matching improve polymorphic code?core▾

instanceof patterns and switch patterns (Java 21) with sealed hierarchies allow exhaustive, type-safe handling without casts, an alternative to visitor patterns.

Follow-ups interviewers ask
  • Example? switch (shape) { case Circle c -> …; case Square s -> …; }.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.