Inheritance & Polymorphism

Project: A Shape Hierarchy

15 min Lesson 10 of 14

Project: A Shape Hierarchy

This final lesson brings together everything you have learned in the tutorial — abstract classes, inheritance, method overriding, polymorphism, and dynamic dispatch — into one small but complete program. You will design a Shape hierarchy, implement two concrete subclasses, and write a utility that works with any shape through a polymorphic list.

The Goal

Build a program that can:

  1. Represent different geometric shapes (circles, rectangles — extendable to more).
  2. Calculate the area of each shape without the caller needing to know its type.
  3. Compute the total area of a mixed collection of shapes.

This is a classic demonstration of why inheritance and polymorphism exist: you write one algorithm (totalArea) that works on shapes you have not even invented yet.

Step 1 — The Abstract Base Class

An abstract class defines the contract that all shapes must fulfill without locking in any specific implementation. area() makes no sense to implement at the Shape level — every subclass will calculate it differently — so we declare it abstract.

public abstract class Shape { // Every concrete shape must implement this public abstract double area(); // A concrete method shared by ALL shapes public String describe() { return getClass().getSimpleName() + " with area = " + String.format("%.2f", area()); } }
Why getClass().getSimpleName()? Because describe() lives on the abstract base class but is called on a subclass instance, getClass() returns the runtime type (Circle, Rectangle, …). This is dynamic dispatch working inside the base class itself.

Step 2 — The Circle Subclass

public class Circle extends Shape { private final double radius; public Circle(double radius) { if (radius <= 0) { throw new IllegalArgumentException("Radius must be positive"); } this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } }
Validate in the constructor. Rejecting bad data early (negative radius) means no invalid Circle objects can ever exist. This keeps your objects always in a consistent, meaningful state.

Step 3 — The Rectangle Subclass

public class Rectangle extends Shape { private final double width; private final double height; public Rectangle(double width, double height) { if (width <= 0 || height <= 0) { throw new IllegalArgumentException("Width and height must be positive"); } this.width = width; this.height = height; } @Override public double area() { return width * height; } }

Step 4 — Polymorphic Total Area

Here is where the payoff happens. The totalArea method accepts a List<Shape> — it does not know or care whether the list contains circles, rectangles, or shapes that do not exist yet. Each call to s.area() dispatches to the correct overridden implementation at runtime.

import java.util.List; public class ShapeCalculator { public static double totalArea(List<Shape> shapes) { double total = 0; for (Shape s : shapes) { total += s.area(); // dynamic dispatch every iteration } return total; } }

Step 5 — Putting It All Together

import java.util.List; public class Main { public static void main(String[] args) { List<Shape> shapes = List.of( new Circle(5), new Rectangle(4, 6), new Circle(3), new Rectangle(10, 2) ); // Print each shape's description (polymorphic call) for (Shape s : shapes) { System.out.println(s.describe()); } // Calculate grand total (polymorphic) double total = ShapeCalculator.totalArea(shapes); System.out.printf("%nTotal area of all shapes: %.2f%n", total); } }

Sample output:

Circle with area = 78.54 Rectangle with area = 24.00 Circle with area = 28.27 Rectangle with area = 20.00 Total area of all shapes: 150.80

Why This Design Works

  • Open/Closed Principle: Adding a Triangle class requires zero changes to ShapeCalculator or Main (beyond adding it to the list). The existing code stays closed to modification but open to extension.
  • Single Responsibility: Circle knows how to compute a circle's area. ShapeCalculator knows how to aggregate. They do not overlap.
  • Polymorphism eliminates if/instanceof chains: Without OOP you would write if (s instanceof Circle) ... else if (s instanceof Rectangle) ... — a fragile block you must update every time a new shape is added.
Avoid switching on type. Code like if (shape instanceof Circle) { ... } else if (shape instanceof Rectangle) { ... } defeats the purpose of polymorphism. If you find yourself writing that pattern, move the behaviour into an overridden method on each subclass instead.

Extending the Hierarchy

To add a Triangle you only need to write one new class — the rest of the program does not change:

public class Triangle extends Shape { private final double base; private final double height; public Triangle(double base, double height) { this.base = base; this.height = height; } @Override public double area() { return 0.5 * base * height; } }

Drop a new Triangle(6, 4) into the list in Main and totalArea handles it without a single other line changed.

Summary

You have built a small but production-shaped hierarchy: an abstract base class defines the contract, concrete subclasses fulfil it, and a polymorphic algorithm operates on any shape through the base type. This pattern — abstract type, multiple implementations, one algorithm — is the foundation of countless real-world Java APIs including Java's own java.io.InputStream, java.util.Collection, and every UI component library ever written.