As you saw before, repeating similar fields or methods in multiple classes can create a maintenance headache. Inheritance addresses this by letting one class "share" its common code with other classes. In Java, the class that provides shared code is called the superclass, while the classes that reuse this code are called subclasses.
Imagine a game where Entity represents anything that can appear in the world. Player and Monster might both need a health field and a takeDamage method. Rather than copy-pasting them into each class, we can define these once in Entity. Subclasses like Player or Monster then inherit the same health and takeDamage logic, but are still free to add unique attributes or special behaviors.
Perhaps the most powerful benefit of inheritance is polymorphism – the ability to treat objects of different subclasses as instances of their common superclass. This enables you to write code that operates on the general type while automatically getting the specialized behavior of specific subtypes:
// Using polymorphism
// Collection holding different entity types
List<Entity> entities = new ArrayList<>();
entities.add(new Player("Hero", 100));
entities.add(new Monster("Goblin", 50));
entities.add(new NPC("Shopkeeper", 30));
// Apply damage to all entities
// Each will use its own version of takeDamage()
for (Entity e : entities) {
e.takeDamage(10); // Polymorphic call
}
In this example, the same takeDamage() call works differently depending on the actual type of entity, even though our code only deals with the general Entity type. This is a powerful mechanism that enables flexible and extensible designs.
The superclass is your general or "parent" class. Subclasses (sometimes called "child" classes) extend this superclass and become more specialized. This relationship is often described as "is-a": a Monster is-a(n) Entity. This helps you follow the DRY principle because you only write the shared logic once.
This diagram shows Entity as the superclass with three subclasses: Player, Monster, and NPC. Each of these subclasses inherits properties and behaviors from Entity, while potentially adding their own specialized features.
In this hierarchy, the Entity class would contain common attributes like position, health, and basic movement functionality. Each subclass then adds specialized behaviors:
Deep inheritance hierarchies: More than 2-3 levels deep creates fragile, difficult-to-maintain code where changes to base classes can have unexpected effects rippling throughout the hierarchy
Inheritance for code reuse only: Using inheritance when there’s no logical "is-a" relationship leads to confusing designs that violate the principle of least surprise
Inheriting from final classes or those not designed for extension: This limits future flexibility and may violate assumptions made by the original class designer
// Incorrect: Using inheritance inappropriately
public class Car extends Engine {
private String model;
// Car functionality...
}
This design is flawed because a Car is not a specialized type of Engine—it contains an engine as one of its components. The consequences of this flawed design include:
// Correct: Using composition for a "has-a" relationship
public class Engine {
private int horsepower;
public void start() {
// Engine starting logic
}
}
public class Car {
private Engine engine; // Composition
private String model;
public Car(Engine engine, String model) {
this.engine = engine;
this.model = model;
}
public void startCar() {
engine.start();
// Additional car startup logic
}
}
With composition, the relationship is clearer: a Car has an Engine, but it is not an Engine. This design also makes it easier to change or replace the engine without affecting the car’s implementation.