Inheritance & Reusability

How Inheritance Promotes Reusability

Inheritance establishes a “IS-A” relationship between classes, allowing a subclass to reuse and extend the attributes and methods of a superclass.

By inheriting common functionality, you avoid duplicating code and can override or extend behaviour where necessary.

Deep Dive: Inheritance

Purpose of Inheritance

Inheritance allows a subclass to automatically acquire the attributes and methods of a superclass, promoting code reuse and logical organisation. Instead of rewriting common functionality, you define it once in the parent class and extend or customise it in child classes.

How It Works

When Class B inherits from Class A:

  • Class B gains all public and protected fields and methods declared in Class A.
  • Class B can add new fields or methods, and override inherited methods to change behaviour.
  • Objects of Class B can be treated as instances of Class A (polymorphism), enabling flexible code.

Access Modifiers and Inheritance

  • public members are inherited and accessible everywhere.
  • protected members are inherited and accessible in subclasses (and within the same package in Java).
  • private members are not inherited: subclasses cannot access them directly.
  • default (package-private in Java) members are inherited only by classes in the same package.

Hierarchy: Vehicle → Car → Regular, Hybrid, Electric

This example shows a Car subclass extending a Vehicle superclass to reuse fuel behaviour. It goes on to show how RegularCar, HybridCar, and ElectricCar then extends Car. Here, we have a hierarchy of Vehicle → Car → Regular, Hybrid, Electric.

Inheritance Hierarchy

The code samples below show how this hierarchy can be implemented.

class Vehicle:
    def __init__(self, fuel: float):
        self._fuel = float(fuel)  # protected by convention

    def start(self):
        print("Vehicle started, fuel:", f"{self._fuel:.2f}")

    def drive(self, km: float):
        self._fuel -= km * 0.1
        print(f"Drove {km} km; remaining fuel: {self._fuel:.2f}")

class Car(Vehicle):
    def __init__(self, make: str, model: str, fuel: float):
        super().__init__(fuel)
        self.make = make
        self.model = model

    def honk(self):
        print(f"{self.make} {self.model} goes 'beep!'")

class RegularCar(Car):
    # A standard gasoline/diesel car using base fuel logic.
    def start(self):
        print(f"RegularCar started: {self.make} {self.model}, fuel: {self._fuel:.2f}")

    def refuel(self, liters: float):
        self._fuel += liters
        print(f"Refueled {liters} L; fuel now: {self._fuel:.2f}")

class ElectricCar(Car):
    # Interpret _fuel as battery charge units (e.g. kWh or %)
    def __init__(self, make: str, model: str, battery: float):
        super().__init__(make, model, battery)

    def start(self):
        print(f"ElectricCar ready: {self.make} {self.model}, battery: {self._fuel:.2f}")

    def drive(self, km: float):
        # EVs consume differently; simple model using 0.15 per km
        consumption_rate = 0.15
        self._fuel -= km * consumption_rate
        print(f"Drove {km} km on battery; remaining charge: {self._fuel:.2f}")

    def recharge(self, amount: float):
        self._fuel += amount
        print(f"Recharged {amount}; battery now: {self._fuel:.2f}")

class HybridCar(Car):
    # Hybrid with both a battery and a fuel tank. _fuel is the liquid fuel; _battery is electric
    def __init__(self, make: str, model: str, fuel: float, battery: float):
        super().__init__(make, model, fuel)
        self._battery = float(battery)

    def start(self):
        print(f"HybridCar started: {self.make} {self.model}, fuel: {self._fuel:.2f}, battery: {self._battery:.2f}")

    def drive(self, km: float):
        # Battery-first strategy: use battery at 0.05/km, then fuel at 0.08/km.
        battery_rate = 0.05
        fuel_rate = 0.08
        max_batt_km = min(km, self._battery / battery_rate if battery_rate > 0 else 0.0)
        self._battery -= max_batt_km * battery_rate
        remaining_km = km - max_batt_km
        self._fuel -= remaining_km * fuel_rate
        print(
            f"Drove {km} km (battery {max_batt_km:.2f} km, fuel {remaining_km:.2f} km); "
            f"remaining -> fuel: {self._fuel:.2f}, battery: {self._battery:.2f}"
        )

    def refuel(self, liters: float):
        self._fuel += liters
        print(f"Refueled {liters} L; fuel now: {self._fuel:.2f}")

    def recharge(self, amount: float):
        self._battery += amount
        print(f"Recharged {amount}; battery now: {self._battery:.2f}")

# --- Simple test (main) ---
if __name__ == "__main__":
    print("== RegularCar ==")
    car = RegularCar("Toyota", "Corolla", 50.0)
    car.start()
    car.drive(20)
    car.honk()
    car.refuel(10)

    print("\n== ElectricCar ==")
    ev = ElectricCar("Tesla", "Model 3", 75.0)
    ev.start()
    ev.drive(50)
    ev.honk()
    ev.recharge(5)

    print("\n== HybridCar ==")
    hv = HybridCar("Toyota", "Prius", 20.0, 10.0)
    hv.start()
    hv.drive(100)
    hv.honk()
    hv.recharge(3)
    hv.refuel(4)
class Vehicle {
    protected double fuel;

    public Vehicle(double fuel) {
        this.fuel = fuel;
    }

    public void start() {
        System.out.println("Vehicle started, fuel: " + String.format("%.2f", fuel));
    }

    public void drive(double km) {
        this.fuel -= km * 0.1;
        System.out.println("Drove " + km + " km; remaining fuel: " + String.format("%.2f", fuel));
    }
}

class Car extends Vehicle {
    protected String make;
    protected String model;

    public Car(String make, String model, double fuel) {
        super(fuel);
        this.make = make;
        this.model = model;
    }

    public void honk() {
        System.out.println(make + " " + model + " goes 'beep!'");
    }
}

class RegularCar extends Car {
    public RegularCar(String make, String model, double fuel) {
        super(make, model, fuel);
    }

    @Override
    public void start() {
        System.out.println("RegularCar started: " + make + " " + model + ", fuel: " + String.format("%.2f", fuel));
    }

    public void refuel(double liters) {
        this.fuel += liters;
        System.out.println("Refueled " + liters + " L; fuel now: " + String.format("%.2f", fuel));
    }
}

class ElectricCar extends Car {
    // Interpret 'fuel' as battery charge units for simplicity.
    public ElectricCar(String make, String model, double battery) {
        super(make, model, battery);
    }

    @Override
    public void start() {
        System.out.println("ElectricCar ready: " + make + " " + model + ", battery: " + String.format("%.2f", fuel));
    }

    @Override
    public void drive(double km) {
        double consumptionRate = 0.15; // per km
        this.fuel -= km * consumptionRate;
        System.out.println("Drove " + km + " km on battery; remaining charge: " + String.format("%.2f", fuel));
    }

    public void recharge(double amount) {
        this.fuel += amount;
        System.out.println("Recharged " + amount + "; battery now: " + String.format("%.2f", fuel));
    }
}

class HybridCar extends Car {
    private double battery;

    public HybridCar(String make, String model, double fuel, double battery) {
        super(make, model, fuel);
        this.battery = battery;
    }

    @Override
    public void start() {
        System.out.println(
            "HybridCar started: " + make + " " + model +
            ", fuel: " + String.format("%.2f", fuel) +
            ", battery: " + String.format("%.2f", battery)
        );
    }

    @Override
    public void drive(double km) {
        double batteryRate = 0.05; // per km
        double fuelRate = 0.08;    // per km

        double maxBattKm = Math.min(km, (batteryRate > 0 ? battery / batteryRate : 0.0));
        battery -= maxBattKm * batteryRate;

        double remainingKm = km - maxBattKm;
        fuel -= remainingKm * fuelRate;

        System.out.println(
            "Drove " + km + " km (battery " + String.format("%.2f", maxBattKm) +
            " km, fuel " + String.format("%.2f", remainingKm) + " km); remaining -> fuel: " +
            String.format("%.2f", fuel) + ", battery: " + String.format("%.2f", battery)
        );
    }

    public void refuel(double liters) {
        this.fuel += liters;
        System.out.println("Refueled " + liters + " L; fuel now: " + String.format("%.2f", fuel));
    }

    public void recharge(double amount) {
        this.battery += amount;
        System.out.println("Recharged " + amount + "; battery now: " + String.format("%.2f", battery));
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("== RegularCar ==");
        RegularCar car = new RegularCar("Toyota", "Corolla", 50.0);
        car.start();
        car.drive(20);
        car.honk();
        car.refuel(10);

        System.out.println("\n== ElectricCar ==");
        ElectricCar ev = new ElectricCar("Tesla", "Model 3", 75.0);
        ev.start();
        ev.drive(50);
        ev.honk();
        ev.recharge(5);

        System.out.println("\n== HybridCar ==");
        HybridCar hv = new HybridCar("Toyota", "Prius", 20.0, 10.0);
        hv.start();
        hv.drive(100);
        hv.honk();
        hv.recharge(3);
        hv.refuel(4);
    }
}

Example: Animal → Dog, Cat

Both Dog and Cat inherit from an abstract Animal type and implement make_sound(). As with all classes, an abstract class is designed to define attributes and methods. However, an object is never instantiated from an abstract class. In this case, it would be hard to imagine creating an object of Animal: what would it be?

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def make_sound(self) -> None:
        pass

class Dog(Animal):
    def make_sound(self) -> None:
        print("Woof!")

class Cat(Animal):
    def make_sound(self) -> None:
        print("Meow!")

# Usage
for pet in (Dog(), Cat()):
    pet.make_sound()
abstract class Animal {
    public abstract void makeSound();
}

class Dog extends Animal {
    @Override public void makeSound() { System.out.println("Woof!"); }
}

class Cat extends Animal {
    @Override public void makeSound() { System.out.println("Meow!"); }
}

public class ZooDemo {
    public static void main(String[] args) {
        Animal[] pets = { new Dog(), new Cat() };
        for (Animal pet : pets) pet.makeSound();
    }
}

Other Inheritance Examples

Employee & Manager

Suppose you have a Employee class with attributes name, id, and salary, plus a method calculatePay(). A Manager subclass can inherit these, then add a department field and override calculatePay() to include bonus calculations:

  • Inherited: name, id, salary, calculatePay()
  • Added: department, calculateBonus()
  • Overridden: calculatePay() to call superclass logic then add bonus

Vehicle & ElectricVehicle

This example describes a slightly different version of the Vehicle example above.

A Vehicle class might define make, model and methods startEngine(), drive(). An ElectricVehicle subclass inherits those then adds an attribute batteryLevel and method chargeBattery(). It can also override drive() to consume battery instead of fuel.

  • Inherited: make, model, startEngine(), drive()
  • Added: batteryLevel, chargeBattery()
  • Overridden: drive() to reduce batteryLevel

 Key Takeaways

  • Inheritance creates a hierarchy that promotes code reuse.
  • Subclasses inherit public and protected members, but not private.
  • Use inheritance judiciously—deep hierarchies can be hard to maintain.
  • Access modifiers control what is inherited and who can invoke it.