Command Palette

Search for a command to run...

Level 1 · 20 min

Inheritance & Polymorphism

Inheritance enables code reuse by deriving new classes from existing ones. Polymorphism allows different classes to be used interchangeably when they share a common interface. Together they enable the open/closed principle.

Inheritance and Overriding

Inheritance (extends) creates an is-a relationship. Method overriding allows a subclass to provide a specific implementation of a method defined in the base class. @Override annotation catches mistakes. Overloading (same name, different parameters) is different — it's resolved at compile time (static dispatch), not runtime (dynamic dispatch).

Polymorphism and Dynamic Dispatch

Polymorphism means 'many forms' — the same method call behaves differently based on the actual object type at runtime. Dynamic dispatch: the JVM uses a virtual method table (vtable) to look up the correct method implementation at runtime. Animal animal = new Dog() → animal.speak() calls Dog's speak(), not Animal's. This is the foundation of the OCP pattern. Bloch's Item 19 (Effective Java) captures the deeper issue: "To document a class so that it can be safely subclassed, you must describe implementation details that should otherwise be left unspecified" — demonstrating that inheritance violates encapsulation by definition. His recommendation: if you haven't designed a class for inheritance, declare it final or prohibit subclassing entirely. The JDK's AbstractList is a well-documented counterexample: its @implSpec tags explicitly describe how iterator() affects remove() so subclasses can override safely.

LSP in Inheritance Hierarchies

LSP: if S is a subtype of T, objects of type T may be replaced with objects of type S without altering correctness. Rectangle.setWidth(w) and setHeight(h) work independently. Square extends Rectangle but can't honor this contract — setting width must also set height. The classic LSP violation. Solution: Square should not extend Rectangle; both should implement a Shape interface.

Key Takeaways

  • Dynamic dispatch (runtime) not static dispatch (compile-time) is what makes polymorphism powerful.
  • Inheritance implies a behavioral contract (LSP), not just code reuse. Use composition when LSP doesn't hold.
  • @Override is mandatory — catches cases where you intended to override but the signature doesn't match.

Code example

// Polymorphism: different implementations, same interface
abstract class Shape {
  abstract double area();
  String describe() { return "Shape with area " + area(); }  // template method
}
class Circle extends Shape {
  private double radius;
  @Override double area() { return Math.PI * radius * radius; }
}
class Rectangle extends Shape {
  private double width, height;
  @Override double area() { return width * height; }
}

// Polymorphic usage — works for any Shape
double totalArea(List<Shape> shapes) {
  return shapes.stream().mapToDouble(Shape::area).sum();
}
// Adding Triangle requires zero changes here