Project: A Shape Hierarchy
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:
- Represent different geometric shapes (circles, rectangles — extendable to more).
- Calculate the area of each shape without the caller needing to know its type.
- 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.
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
Circle objects can ever exist. This keeps your objects always in a consistent, meaningful state.
Step 3 — The Rectangle Subclass
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.
Step 5 — Putting It All Together
Sample output:
Why This Design Works
- Open/Closed Principle: Adding a
Triangleclass requires zero changes toShapeCalculatororMain(beyond adding it to the list). The existing code stays closed to modification but open to extension. - Single Responsibility:
Circleknows how to compute a circle's area.ShapeCalculatorknows how to aggregate. They do not overlap. - Polymorphism eliminates
if/instanceofchains: Without OOP you would writeif (s instanceof Circle) ... else if (s instanceof Rectangle) ...— a fragile block you must update every time a new shape is added.
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:
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.