Inheritance: Code Reuse

Duration: 45 min

Inheritance: Code Reuse

Duration: 45 min

Introduction

Inheritance allows you to create new classes based on existing ones, inheriting their fields and methods. This eliminates code duplication and establishes relationships between classes. A class can extend another to specialize its behavior or add new functionality. Inheritance enables the IS-A relationship: a Dog IS-A Animal, a SavingsAccount IS-A BankAccount.

This module covers extending classes with extends, method overriding, the super keyword, abstract classes, protected access, and designing class hierarchies. You'll learn when inheritance is appropriate and how to use it effectively.

Extends Keyword and Basic Inheritance

The extends keyword creates a parent-child relationship. The child (subclass) inherits all public and protected members from the parent (superclass).

// Parent class (superclass)
public class Vehicle {
    protected String brand;  // Protected - accessible to subclasses
    protected int year;
    
    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year = year;
    }
    
    public void start() {
        System.out.println(brand + " engine starting...");
    }
    
    public void stop() {
        System.out.println(brand + " engine stopping...");
    }
    
    public void describe() {
        System.out.println("Vehicle: " + brand + " (" + year + ")");
    }
}

// Child class (subclass) public class Car extends Vehicle { private int numDoors; // Constructor must call super() public Car(String brand, int year, int numDoors) { super(brand, year); // Call parent constructor this.numDoors = numDoors; } // New method in subclass public void openTrunk() { System.out.println("Trunk opened"); } // Override parent method @Override public void describe() { super.describe(); // Call parent version System.out.println("Doors: " + numDoors); } }

public class InheritanceDemo { public static void main(String[] args) { Car myCar = new Car("Toyota", 2023, 4); myCar.start(); // Inherited method myCar.openTrunk(); // New method myCar.describe(); // Overridden method myCar.stop(); // Inherited method } }

The child class automatically gets all parent methods and fields. You can override methods to change behavior. Use super() to call the parent constructor and super.methodName() to call parent methods.

Method Overriding

Overriding replaces a parent method with a specialized version in the child. The signature must match exactly; only the body changes.

public class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public void makeSound() {
        System.out.println("Some generic animal sound");
    }
    
    public void sleep() {
        System.out.println(name + " is sleeping");
    }
}

public class Dog extends Animal { public Dog(String name) { super(name); } // Override to provide dog-specific implementation @Override public void makeSound() { System.out.println(name + " barks: Woof! Woof!"); } }

public class Cat extends Animal { public Cat(String name) { super(name); } // Override with cat-specific implementation @Override public void makeSound() { System.out.println(name + " meows: Meow!"); } }

public class PolymorphismDemo { public static void main(String[] args) { Animal dog = new Dog("Rex"); Animal cat = new Cat("Whiskers"); dog.makeSound(); // "Rex barks" cat.makeSound(); // "Whiskers meows" dog.sleep(); // "Rex is sleeping" (inherited, not overridden) cat.sleep(); // "Whiskers is sleeping" (inherited) } }

The @Override annotation is optional but recommended—it helps catch mistakes (e.g., typos in method name). Overridden methods maintain the same signature but different implementation.

Super Keyword

super refers to the parent class. Use it to call parent constructors and methods.

public class Employee {
    protected String name;
    protected double salary;
    
    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
    
    public double getAnnualBonus() {
        return salary * 0.1;  // 10% bonus
    }
    
    public void displayInfo() {
        System.out.println("Employee: " + name + ", Salary: $" + salary);
    }
}

public class Manager extends Employee { private int teamSize; public Manager(String name, double salary, int teamSize) { super(name, salary); // Call parent constructor this.teamSize = teamSize; } @Override public double getAnnualBonus() { // Call parent implementation and add extra bonus double baseBonus = super.getAnnualBonus(); double teamBonus = 1000 * teamSize; return baseBonus + teamBonus; } @Override public void displayInfo() { super.displayInfo(); // Call parent implementation first System.out.println("Team size: " + teamSize); System.out.println("Annual bonus: $" + getAnnualBonus()); } }

public class SuperDemo { public static void main(String[] args) { Employee emp = new Employee("Alice", 50000); emp.displayInfo(); System.out.println("Bonus: $" + emp.getAnnualBonus()); System.out.println(); Manager mgr = new Manager("Bob", 80000, 5); mgr.displayInfo(); } }

Using super allows you to build on parent behavior instead of completely replacing it. This is powerful for specialization—extend functionality rather than reimplementing from scratch.

Abstract Classes

Abstract classes cannot be instantiated; they provide a template for subclasses. Use abstract for classes and methods.

public abstract class Shape {
    protected String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    // Abstract method - subclass must implement
    public abstract double getArea();
    
    public abstract double getPerimeter();
    
    // Concrete method - shared by all subclasses
    public void describe() {
        System.out.println("Shape color: " + color);
        System.out.printf("Area: %.2f, Perimeter: %.2f%n", getArea(), getPerimeter());
    }
}

public class Circle extends Shape { private double radius; public Circle(String color, double radius) { super(color); this.radius = radius; } @Override public double getArea() { return Math.PI radius radius; } @Override public double getPerimeter() { return 2 Math.PI radius; } }

public class Rectangle extends Shape { private double width; private double height; public Rectangle(String color, double width, double height) { super(color); this.width = width; this.height = height; } @Override public double getArea() { return width * height; } @Override public double getPerimeter() { return 2 * (width + height); } }

public class AbstractDemo { public static void main(String[] args) { // Shape s = new Shape("red"); // ERROR: Cannot instantiate abstract class Shape circle = new Circle("red", 5); circle.describe(); Shape rect = new Rectangle("blue", 4, 6); rect.describe(); } }

Abstract classes enforce that subclasses provide implementations for critical methods. They're perfect for defining class hierarchies where subclasses have different implementations but shared behavior.

Protected Access

protected members are accessible to subclasses and other classes in the same package. It's more restrictive than public but less restrictive than private.

public class BankAccount {
    private String accountNumber;    // Only in this class
    protected double balance;        // Subclasses can access
    public String accountHolder;     // Everywhere
    
    protected BankAccount(String accountNumber, double balance) {
        this.accountNumber = accountNumber;
        this.balance = balance;
    }
    
    protected void updateBalance(double amount) {
        balance += amount;
    }
}

public class SavingsAccount extends BankAccount { private double interestRate; public SavingsAccount(String accountNumber, double balance, double rate) { super(accountNumber, balance); this.interestRate = rate; } public void addInterest() { // Can access protected balance updateBalance(balance * interestRate); System.out.println("Interest added. New balance: $" + balance); } public void display() { // Cannot access private accountNumber // System.out.println(accountNumber); // ERROR // Can access protected balance System.out.println("Savings Account Balance: $" + balance); System.out.println("Interest Rate: " + (interestRate * 100) + "%"); } }

Protected methods are part of the subclass interface—they're intended for subclass use. Private methods are implementation details—subclasses shouldn't depend on them.

Design Principles

IS-A Relationship

Inheritance represents an IS-A relationship. Use it only when genuinely true.

// Good: Dog IS-A Animal (true relationship)
class Dog extends Animal { }

// Bad: Car IS-A Engine (not true - Car HAS-A Engine) class Car extends Engine { } // Wrong!

// Correct approach for HAS-A: class Car { private Engine engine; }

Liskov Substitution Principle

Subclasses should be usable wherever parent classes are expected.

// This violates LSP
class Bird {
    public void fly() { }
}

class Penguin extends Bird { @Override public void fly() { throw new UnsupportedOperationException("Penguins can't fly"); } }

// Better approach: class Bird { public void move() { } }

class Penguin extends Bird { @Override public void move() { System.out.println("Penguin waddles"); } }

Hierarchical Class Design

Multi-Level Inheritance

public class HierarchicalInheritance {
    abstract static class Vehicle {
        protected String brand;
        abstract void start();
    }
    
    abstract static class Car extends Vehicle {
        protected int numDoors;
        abstract void honk();
    }
    
    static class Sedan extends Car {
        private boolean isAutomatic;
        
        public Sedan(String brand, boolean isAutomatic) {
            this.brand = brand;
            this.numDoors = 4;
            this.isAutomatic = isAutomatic;
        }
        
        @Override
        void start() {
            System.out.println(brand + " sedan starting");
        }
        
        @Override
        void honk() {
            System.out.println("Horn: Beep beep");
        }
    }
}

Template Method Pattern

public class TemplateMethod {
    abstract static class DataProcessor {
        // Template method - defines algorithm structure
        public final void processData(String input) {
            String validated = validate(input);
            String transformed = transform(validated);
            String enriched = enrich(transformed);
            display(enriched);
        }
        
        // These must be implemented by subclasses
        protected abstract String validate(String input);
        protected abstract String transform(String input);
        protected abstract String enrich(String input);
        
        // Common implementation
        protected void display(String output) {
            System.out.println("Result: " + output);
        }
    }
    
    static class CSVProcessor extends DataProcessor {
        @Override
        protected String validate(String input) {
            return input.trim();
        }
        
        @Override
        protected String transform(String input) {
            return input.replace(",", " | ");
        }
        
        @Override
        protected String enrich(String input) {
            return "[CSV] " + input;
        }
    }
    
    static class JSONProcessor extends DataProcessor {
        @Override
        protected String validate(String input) {
            return input.trim();
        }
        
        @Override
        protected String transform(String input) {
            return "{\"data\": \"" + input + "\"}";
        }
        
        @Override
        protected String enrich(String input) {
            return input;  // Already in JSON
        }
    }
}

Method Overriding Best Practices

Respecting Liskov Substitution Principle

public class LSPDemo {
    abstract static class Shape {
        abstract double getArea();
    }
    
    static class Rectangle extends Shape {
        private double width;
        private double height;
        
        public Rectangle(double width, double height) {
            this.width = width;
            this.height = height;
        }
        
        @Override
        double getArea() {
            return width * height;
        }
    }
    
    static class Circle extends Shape {
        private double radius;
        
        public Circle(double radius) {
            this.radius = radius;
        }
        
        @Override
        double getArea() {
            return Math.PI  radius  radius;
        }
    }
    
    // Can use any Shape subclass interchangeably
    static void printShapeArea(Shape shape) {
        System.out.println("Area: " + shape.getArea());
    }
}

Composition vs Inheritance

public class CompositionVsInheritance {
    // Problem with inheritance: rigid hierarchy
    static class Dog extends Animal {
        void bark() { }
    }
    
    // Better solution: composition for flexibility
    interface Behavior {
        void perform();
    }
    
    static class BarkingBehavior implements Behavior {
        public void perform() {
            System.out.println("Woof!");
        }
    }
    
    static class AnimalComposed {
        private Behavior behavior;
        
        public AnimalComposed(Behavior behavior) {
            this.behavior = behavior;
        }
        
        public void act() {
            behavior.perform();
        }
    }
}

Inheritance Patterns and Anti-Patterns

Good Pattern: Single Responsibility

abstract static class DatabaseConnection {
    abstract void connect();
    abstract void query(String sql);
}

static class MySQLConnection extends DatabaseConnection { @Override void connect() { System.out.println("MySQL connect"); } @Override void query(String sql) { System.out.println("MySQL query"); } }

static class PostgreSQLConnection extends DatabaseConnection { @Override void connect() { System.out.println("PostgreSQL connect"); } @Override void query(String sql) { System.out.println("PostgreSQL query"); } }

Anti-Pattern: Fragile Base Class

static class BaseClass {
    protected void process() {
        preprocessData();
        doProcess();  // If subclass overrides this, bugs may occur
        postprocessData();
    }
    
    protected void preprocessData() { }
    protected void doProcess() { }
    protected void postprocessData() { }
}

// Subclass breaks parent's assumptions static class BrokenSubclass extends BaseClass { @Override protected void doProcess() { // Forgot to call methods or changed timing doProcess(); } }

Protected Access Gotchas

public class ProtectedAccess {
    public static class Parent {
        protected String protectedField = "parent";
        
        protected void protectedMethod() {
            System.out.println("Parent method");
        }
    }
    
    public static class Child extends Parent {
        public void demonstrateAccess() {
            // Can access parent's protected members
            System.out.println(protectedField);
            protectedMethod();
        }
    }
    
    public static void main(String[] args) {
        Parent parent = new Parent();
        // Cannot access protected from outside
        // parent.protectedField;  // ERROR
        // parent.protectedMethod();  // ERROR
        
        // But can access through subclass
        Child child = new Child();
        child.demonstrateAccess();  // Works
    }
}

Key Takeaways

1. extends creates parent-child relationships 2. Subclasses inherit all public and protected members 3. Override methods to specialize behavior 4. super() calls parent constructor; super.method() calls parent method 5. Abstract classes define templates; subclasses must implement abstract methods 6. protected access allows subclass access while hiding implementation details 7. Inheritance represents IS-A relationships 8. Subclasses should work everywhere parent classes do (Liskov Substitution) 9. Composition often provides more flexibility than inheritance 10. Use inheritance for is-a relationships; composition for has-a relationships 11. Template method pattern uses inheritance for algorithmic structure 12. Be careful with protected access—it binds subclasses to implementation details

Quiz

Question 1: What happens if you don't call super() in a subclass constructor?

  • A) Java automatically calls it ✓
  • B) Compilation error
  • C) Runtime error
  • D) The parent is not initialized

Question 2: Can you instantiate an abstract class?

  • A) Yes, always
  • B) Yes, if it has implemented methods
  • C) No, never ✓
  • D) Only through subclasses

Question 3: What's the difference between private and protected?

  • A) Same thing
  • B) protected allows subclass access ✓
  • C) private allows subclass access
  • D) protected is slower

Question 4: What does super.methodName() do?

  • A) Calls a method on the current object
  • B) Calls parent class implementation ✓
  • C) Creates a new super object
  • D) Calls a static method

Question 5: Which statement is true about method overriding?

  • A) Return type can change
  • B) Parameter list can change
  • C) The method signature must match parent ✓
  • D) You can override private methods

Question 6: What's the IS-A relationship?

  • A) Inheritance relationship ✓
  • B) Composition relationship
  • C) A method overriding
  • D) Object instantiation