Skip to content
Part IA Michaelmas Term

What is Inheritance?

The “Is-A” Relationship

Inheritance models a hierarchical “is-a” relationship between classes. A subclass is-a kind of its superclass — it can do everything the superclass can do, plus potentially more.

class Animal {
    void eat() { System.out.println("Eating"); }
}

class Dog extends Animal {
    void bark() { System.out.println("Woof"); }
}

Here, a Dog is-a Animal. Every Dog can eat() (inherited behaviour) and also bark() (specialised behaviour). You can use a Dog anywhere an Animal is expected.

Is-a relationship tree showing inheritance hierarchy

Single Class Inheritance

Java uses single class inheritance: each class extends exactly one superclass via the extends keyword. Java does not permit inheriting from multiple classes (see the Multiple Inheritance and Diamond Problem notes for why).

Every class that does not explicitly name a superclass implicitly extends Object:

class Foo { }  // implicitly: class Foo extends Object { }

This means every Java object — regardless of how it is defined — inherits the methods of Object:

MethodPurpose
toString()Returns a string representation of the object
equals(Object)Tests logical equality (default: identity)
hashCode()Returns a hash code for use in hash-based collections
getClass()Returns the runtime Class object for the object’s type
clone()Creates and returns a copy (protected, rarely used directly)
finalize()Called by GC before reclamation (deprecated since Java 9)

Transitivity

The “is-a” relationship is transitive: if Dog extends Animal and Animal extends Object, then Dog is-a Animal and Dog is-a Object. A Dog reference can be passed to any method expecting an Animal or an Object.

What Inheritance Provides

Code Reuse

Common behaviour lives in the superclass and is automatically available to all subclasses. The eat() method in Animal is written once and shared by Dog, Cat, Bird, and any future animal subclass. This avoids code duplication and ensures that changes to shared behaviour are made in one place.

Polymorphic Substitution

A single variable of the superclass type can hold objects of any subclass:

Animal a = new Dog();
a.eat();  // calls Dog's eat() if overridden, or Animal's if not

This is the foundation of the Open-Closed Principle — code written against Animal works with any new subclass without modification.

Interface Inheritance

A subclass inherits the superclass’s type — it is a subtype. A method declared to accept Animal will accept Dog, Cat, etc. This lets you write general code:

void feed(Animal a) {
    a.eat();
}

The feed method works with any current or future animal subtype.

The Costs of Inheritance

Inheritance is powerful but introduces tight coupling between the subclass and its superclass. This coupling can create problems:

Fragile Base Class Problem

Changing the superclass can inadvertently break subclasses. If you add a new method to Animal that happens to match the signature of a new method in Dog, the Dog method unintentionally overrides the superclass method (or vice versa). If you change the implementation of an inherited method, all subclasses are affected — possibly in ways the subclass author did not anticipate.

Deep Hierarchies

A shallow hierarchy (2–3 levels) is manageable. A deep hierarchy (Vehicle → Car → Sedan → LuxurySedan → ...) becomes hard to understand, test, and modify. The behaviour of a class at the bottom of a deep hierarchy is spread across many superclasses.

The “Is-A” Trap

Not every conceptual “is-a” should be modelled as inheritance. A Square is-a Rectangle geometrically, but making Square extends Rectangle breaks the Liskov Substitution Principle (see the Square-Rectangle notes). The question is not “does X seem like a kind of Y?” but “does X honour Y’s behavioural contract in every respect?”

When to Use Inheritance

Inheritance is appropriate when:

  • A genuine, stable “is-a” relationship exists that will not change over time.
  • The superclass defines a contract that subclasses are expected to fulfil (the template-method pattern, abstract classes).
  • You need polymorphic substitution — code must work with a family of related types through a common interface.

When these conditions do not hold, composition (holding a reference to an object of another class as a field) is often a better choice. “Favour composition over inheritance” is a well-known design guideline for a reason.