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.
🎯 What you'll learn
extendsand Single Inheritancesuper— 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.
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 Animal2super — 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.
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.
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.
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 cast5Abstract Classes
An abstract class cannot be instantiated and may declare methods with no body (abstract methods) that subclasses are forced to implement.
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.
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 class | Interface | |
|---|---|---|
| Instance fields | yes | only public static final constants |
| Constructors | yes | no |
| Multiple inheritance | no (single extends) | yes (multiple implements) |
| Method bodies | yes (mix of abstract + concrete) | yes, via default/static (Java 8+) |
| When to use | shared state + a strong "is-a" hierarchy | a 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.
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 + ")";
}
}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
- 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:
invokespecialfor the constructor happens first, but the vtable slot for the overridden method already points at the subclass's version). - Overriding
equals()withouthashCode()(or vice versa) — violates the contract that equal objects must have equal hash codes, silently breakingHashMap/HashSetlookups (see the Collections Framework topic) even though nothing throws an exception. - Assuming fields are polymorphic like methods —
base.labelin section 4 resolves by declared type, not runtime type; this trips up anyone coming from a language where all member access is virtual. - 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
@Overridethe compiler won't warn you; the superclass version silently keeps running for callers using the superclass type. - 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
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
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.
- 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.
- 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.
- 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.
- 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.
- 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.