Lab 07: Inheritance & Polymorphism

Objective

Use extends to build class hierarchies, override methods, call super, understand polymorphism, and apply the Liskov Substitution Principle.

Background

Inheritance lets you build specialized classes from general ones, reusing and extending behavior. Polymorphism — "many forms" — means a reference of type Animal can hold a Dog or Cat, and the correct method runs at runtime. These mechanisms enable frameworks, plugin systems, and extensible designs.

Time

40 minutes

Prerequisites

  • Lab 06 (OOP — Classes)

Tools

  • Java 21 (Eclipse Temurin)

  • Docker image: innozverse-java:latest


Lab Instructions

Step 1: extends and method overriding

💡 @Override annotation is crucial — it asks the compiler to verify you're actually overriding a parent method. Without it, a typo like pubilc double Area() silently creates a new method instead of overriding, and the bug is invisible until runtime.

📸 Verified Output:


Step 2: super — Calling Parent Methods

💡 super.method() calls the parent's implementation from within an override. This is the "extend, don't replace" pattern. ElectricVehicle.status() adds battery info to the vehicle's status string without duplicating the base formatting logic.

📸 Verified Output:


Step 3: Polymorphism in Practice

💡 processAll doesn't know what type of payment it's processing — it just calls p.process() and p.getMethod(). Adding a new payment type (Apple Pay, Stripe) requires zero changes to processAll. This is Open/Closed Principle: open for extension, closed for modification.

📸 Verified Output:


Step 4: Abstract Classes vs Concrete Classes

💡 The Template Method Pattern uses final on the skeleton method to prevent overriding the algorithm structure, while abstract methods let subclasses customize each step. final on a method means "I define the algorithm; you fill in the blanks."

📸 Verified Output:


Step 5: final, sealed Classes

💡 Sealed interfaces (Java 17+) list all permitted implementations. Combined with pattern matching switch, the compiler verifies your switch is exhaustive — you can't forget to handle a case. This is how Rust's enums and Haskell's ADTs work; Java finally caught up.

📸 Verified Output:


Step 6: Method Hiding vs Overriding

💡 Static methods are not polymorphic. They resolve based on the compile-time type of the reference, not the runtime type of the object. This is why "method hiding" (for statics) is a separate concept from "method overriding" (for instance methods). Avoid calling static methods via instance references — it's confusing.

📸 Verified Output:


Step 7: Covariant Return Types & Fluent Inheritance

💡 Covariant return types (Java 5+) let overriding methods return a more specific type. Cat.create() returns Cat instead of Animal — callers who know they have a Cat get back a Cat without casting. Fluent builders use the self-type pattern <T extends Builder<T>> to preserve the subtype through method chains.

📸 Verified Output:


Step 8: Full Example — Employee Hierarchy

💡 Manager.calculatePay() calls super.calculatePay() to reuse the monthly calculation logic and add the bonus on top. This is the key benefit of inheritance: you add or modify behavior without copying code. When baseSalary changes in FullTime, Manager automatically benefits.

📸 Verified Output:


Verification

Summary

You've covered extends, @Override, super, abstract classes, polymorphism, sealed interfaces, static vs instance dispatch, covariant returns, and the employee payroll hierarchy. Inheritance is powerful — use it when you have a genuine "is-a" relationship and need to share behavior, not just to reuse code.

Further Reading

Last updated