Inheritance

Object-Oriented Programming (OOP)

Imagine you're building a system to manage a fleet of vehicles. You start by creating a Car class and a Truck class. You quickly notice they share a lot of common code:

public class Car {
    private String licensePlate;
    private int speed;

    public void startEngine() { /* ... */ }
    public void stopEngine() { /* ... */ }
    public void accelerate() { /* ... */ }
    // Car-specific method
    public void openTrunk() { /* ... */ }
}

public class Truck {
    private String licensePlate; // Duplicate
    private int speed;           // Duplicate

    public void startEngine() { /* ... */ } // Duplicate
    public void stopEngine() { /* ... */ }  // Duplicate
    public void accelerate() { /* ... */ } // Duplicate
    // Truck-specific method
    public void lowerTailgate() { /* ... */ }
}

This duplication is inefficient and a maintenance nightmare. A change to the startEngine logic would need to be made in both places. The natural solution is to extract this shared code into a common base class, which brings us to inheritance.

Inheritance: The "Is-A" Relationship

Inheritance is a mechanism where a new class (the subclass or child) acquires the properties and behaviors of an existing class (the superclass or parent).

The most important concept to remember is that inheritance represents a true "is-a" relationship.

  • A Car is a Vehicle.
  • A Truck is a Vehicle.
  • A CheckingAccount is an Account.

If this "is-a" test doesn't feel right, inheritance is likely the wrong tool for the job. You achieve it in code using the extends keyword, and you can reuse or redefine parent behaviors using method overriding (@Override).

A Practical Example: The Vehicle Hierarchy

Let's refactor our previous example using an abstract Vehicle base class.

// The abstract base class contains the shared code
public abstract class Vehicle {
    protected String licensePlate; // 'protected' is often used to give subclasses access
    protected int speed;

    public void startEngine() {
        System.out.println("Engine starting...");
    }

    public void accelerate() {
        this.speed += 10;
    }

    // An abstract method that all subclasses MUST implement
    public abstract double getTollFee();
}

// The Car subclass inherits the common code
public class Car extends Vehicle {
    // Car-specific method
    public void openTrunk() { /* ... */ }

    @Override
    public double getTollFee() {
        return 2.50; // Cars pay a standard toll
    }
}

// The Truck subclass also inherits the common code but has its own logic
public class Truck extends Vehicle {
    private int cargoWeight;

    // Truck-specific method
    public void lowerTailgate() { /* ... */ }

    @Override
    public double getTollFee() {
        // Trucks pay a higher toll based on weight
        return 5.00 + (cargoWeight * 0.01);
    }
}

This is much cleaner. We've eliminated duplicate code, and each class is now responsible only for its unique logic.

Interview Focus: The "Composition over Inheritance" Principle

This is one of the most important design principles you can discuss in an interview. While inheritance is powerful, it has serious drawbacks, and experienced developers use it cautiously.

The Problems with Inheritance:

  1. Tight Coupling: The subclass is intrinsically linked to the superclass's implementation. A seemingly small change in the Vehicle class could unexpectedly break the Car or Truck subclasses. This is known as the Fragile Base Class Problem.
  2. Inflexible Hierarchies: The real world is messy. What if you need to model a FlyingCar? Should it extend Car or Aircraft? What about an AmphibiousVehicle? Class hierarchies can quickly become deep, rigid, and confusing.
  3. Encapsulation Can Be Violated: By using protected members, you are breaking the "black box" nature of the superclass and allowing subclasses to depend on its internal implementation details.

The Alternative: Composition (The "Has-A" Relationship)

Instead of a class being something, it can have something.

  • A Car is not an Engine; a Car has an Engine.
  • A User is not an Address; a User has an Address.

Composition is the practice of building complex objects by "composing" them from other, smaller objects.

// Example of Composition
public interface Engine {
    void start();
}

public class GasEngine implements Engine {
    @Override public void start() { /* ... */ }
}
public class ElectricEngine implements Engine {
    @Override public void start() { /* ... */ }
}

public class Car {
    // The Car HAS-AN Engine. It is "composed" with an Engine object.
    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine; // The specific engine is injected
    }

    public void start() {
        this.engine.start(); // The car delegates the "start" behavior to its engine component.
    }
}

// Now your code is flexible
Car gasCar = new Car(new GasEngine());
Car electricCar = new Car(new ElectricEngine());

With composition, our Car is not tied to one type of engine. It's more flexible, easier to test, and avoids the fragile base class problem. This leads to the guideline: "Favor Composition over Inheritance."

Use inheritance for genuine "is-a" relationships and polymorphism. Use composition for code reuse and acquiring new capabilities.

Inheritance and the Liskov Substitution Principle

The most crucial concept linked to inheritance is the Liskov Substitution Principle (LSP), the 'L' in SOLID (We'll discuss SOLID principles in detail later).

LSP states: Subtypes must be substitutable for their base types without altering the correctness of the program.

In simple terms, if you have a piece of code that works with a Vehicle, it should also work perfectly if you pass it a Car or a Truck. Your overridden getTollFee method is a good example of LSP in action.

A classic violation is the "Square-Rectangle" problem. A Square "is-a" Rectangle. But if you have a setHeight(h) method on Rectangle, and you call it on a Square object, you must also change the width, which a user of the Rectangle object would not expect. This breaks substitutability.

LSP is the litmus test for inheritance. If your hierarchy violates LSP, you should almost certainly be using composition instead.