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 aVehicle
. - A
Truck
is aVehicle
. - A
CheckingAccount
is anAccount
.
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:
- Tight Coupling: The subclass is intrinsically linked to the superclass's implementation. A seemingly small change in the
Vehicle
class could unexpectedly break theCar
orTruck
subclasses. This is known as the Fragile Base Class Problem. - Inflexible Hierarchies: The real world is messy. What if you need to model a
FlyingCar
? Should it extendCar
orAircraft
? What about anAmphibiousVehicle
? Class hierarchies can quickly become deep, rigid, and confusing. - 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 anEngine
; aCar
has anEngine
. - A
User
is not anAddress
; aUser
has anAddress
.
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.