AlgoInsight

Free lesson · Java Basic To Advance

Introduction to Inheritance

Medium9 min readjava

The "Is-A" Relationship

Module 7 covered single classes — their state, behavior, encapsulation, and lifecycle. This Module covers relationships between classes. Specifically, inheritance — the mechanism by which one class extends another, reusing and customizing its behavior.

Inheritance is the second of the four pillars of OOP we introduced in Topic 7.1. It's also the most-debated pillar — used poorly, it creates brittle code that's hard to refactor. Used well, it's a powerful mechanism for sharing code and expressing relationships between concepts.

This Topic introduces inheritance conceptually. Topics 8.2 through 8.13 cover the mechanics — extends, super, overriding, polymorphism, and the patterns that come up in production.

I'm Brandon, continuing through Module 8. Inheritance is where I've seen the most expensive mistakes in production codebases — and where the biggest reputational damage occurs in design reviews. Getting it right matters.

What Inheritance Is

The core idea: one class extends another, automatically gaining its fields and methods, with the option to add new ones or override existing ones.

public class Animal {
    protected String name;
    protected int age;

    public void breathe() {
        System.out.println(name + " is breathing");
    }

    public void sleep() {
        System.out.println(name + " is sleeping");
    }
}

public class Dog extends Animal {
    private String breed;

    public void bark() {
        System.out.println(name + " says Woof!");
    }
}

Dog rex = new Dog();
rex.name = "Rex";       // inherited from Animal
rex.age = 3;             // inherited from Animal
rex.breathe();           // inherited method
rex.sleep();             // inherited method
rex.bark();              // Dog-specific method

Dog extends Animal means Dog automatically has everything Animal has — name, age, breathe(), sleep() — plus whatever Dog adds (breed, bark()).

This relationship is called "is-a" — a Dog is an Animal. Anywhere code expects an Animal, you can pass a Dog. Dog is a more specific kind of Animal.

Interactive animation: IS-A gets a parent and HAS-A gets a field - a Dog is an Animal, while a Car has an Engine.

The Vocabulary

Inheritance comes with specific terminology:

In our example: Animal is the superclass. Dog is the subclass. Dog inherits Animal's members. The Animal-Dog relationship forms a two-level hierarchy.

Java has single inheritance for classes — a class can extend at most one direct superclass. We'll cover why (and the workaround — interfaces) in Topic 8.11.

Why Inheritance Exists

Three primary uses for inheritance:

1. Code Reuse

The most obvious benefit. Common code lives in the superclass; subclasses don't repeat it.

public class Vehicle {
    protected double fuelLevel;
    protected double maxFuel;

    public void refuel(double amount) {
        fuelLevel = Math.min(fuelLevel + amount, maxFuel);
    }

    public boolean hasFuel() {
        return fuelLevel > 0;
    }
}

public class Car extends Vehicle {
    // Cars get refuel() and hasFuel() for free
}

public class Motorcycle extends Vehicle {
    // So do motorcycles
}

If we add a getFuelPercentage() method to Vehicle, all subclasses get it automatically. Adding behavior in one place propagates everywhere it should.

2. Polymorphism

Inheritance enables polymorphic dispatch (covered in detail in Topic 8.6). Code written against the superclass type works with any subclass:

List<Animal> zoo = new ArrayList<>();
zoo.add(new Dog());
zoo.add(new Cat());
zoo.add(new Bird());

for (Animal a : zoo) {
    a.breathe();   // each calls the appropriate version
}

The loop doesn't care what specific kind of Animal is in the list. New animal types can be added without changing this code.

Interactive animation: One describe(Animal a) method serves a Dog, a Cat and an Axolotl written years later.

3. Domain Modeling

Sometimes the problem domain has inherent inheritance structure:

public abstract class PaymentMethod { ... }
public class CreditCard extends PaymentMethod { ... }
public class BankTransfer extends PaymentMethod { ... }
public class CryptoWallet extends PaymentMethod { ... }

A credit card IS a payment method. A bank transfer IS a payment method. The hierarchy mirrors the real-world concept. Code that handles "any payment method" can work with any subtype.

This is the Stripe payment-method abstraction I mentioned in Topic 7.1. The inheritance structure isn't OOP for its own sake — it captures the actual domain.

The Inheritance Trap

Inheritance is also one of the most-misused OOP features. The trap: inheritance for code reuse alone, when the is-a relationship doesn't hold.

A canonical bad example:

public class Stack extends ArrayList {
    public void push(Object item) { add(item); }
    public Object pop() { return remove(size() - 1); }
}

This works mechanically, but it's wrong design. A Stack is NOT an ArrayList. ArrayList exposes methods like add(index, item), set(index, value), remove(index) that violate Stack semantics — users could insert items in the middle, breaking the LIFO contract.

The right design uses composition:

public class Stack {
    private ArrayList<Object> items = new ArrayList<>();

    public void push(Object item) { items.add(item); }
    public Object pop() { return items.remove(items.size() - 1); }
}

Now Stack has-an ArrayList but doesn't expose ArrayList's broader interface. The Stack contract is preserved.

This is composition vs inheritance — one of the most important design distinctions in OOP. We cover it in detail in Topic 8.12.

The general rule: inheritance models "is-a." Composition models "has-a." Inheritance is for genuine subtype relationships; composition is for code reuse without subtyping.

graph TD
    A["Considering inheritance?"] --> B{"Is-a relationship?"}

    B -->|"Dog IS-A Animal"| C["Inheritance is appropriate"]
    B -->|"Stack HAS-A list"| D["Use composition instead"]
    B -->|"Just want code reuse"| D
    B -->|"Need polymorphism"| C

    C --> E["Use extends"]
    D --> F["Store as field, delegate as needed"]

    style C fill:#10b981,color:#fff
    style D fill:#dc2626,color:#fff
This diagram shows when inheritance is appropriate. The key test is the is-a relationship — Dog is an Animal, so inheritance fits. If the relationship is has-a — Stack has a list of items — composition is the right choice. Pure code reuse without a genuine is-a relationship is the most common inheritance abuse, and composition handles it better. When polymorphic behavior is needed — same code working with different concrete types — inheritance enables that elegantly. The two approaches aren't enemies; they solve different problems. Inheritance for type hierarchies, composition for code reuse and configuration.

The Inheritance Hierarchy in Java

Every Java class implicitly extends another class. If you don't specify, the implicit parent is java.lang.Object:

public class User { }
// is equivalent to
public class User extends Object { }

This is why every Java class has equals(), hashCode(), toString(), and the other Object methods — they're inherited from the universal root (Topic 7.9, Topic 8.10).

Hierarchies can be deeper:

Object
└── Number
    ├── Integer
    ├── Long
    ├── Double
    └── BigDecimal

Or:

Object
└── Throwable
    ├── Error
    └── Exception
        ├── RuntimeException
        │   ├── NullPointerException
        │   └── IllegalArgumentException
        └── IOException
            └── FileNotFoundException

The Java standard library uses inheritance extensively for exceptions, numbers, collections, and many other structures. Understanding this hierarchy is important for using the library effectively.

What Subclasses Inherit

A subclass inherits:

A subclass does NOT inherit:

public class Vehicle {
    public String name;          // inherited
    protected double fuelLevel;  // inherited
    private String vin;          // NOT inherited

    public void start() { ... }  // inherited
    protected void log() { ... } // inherited
    private void encrypt() { ... } // NOT inherited

    public Vehicle() { ... }     // NOT inherited (but must be called)
}

public class Car extends Vehicle {
    public void drive() {
        System.out.println(name);    // OK — inherited
        // System.out.println(vin);  // compile error — vin is private
    }
}

Private members are private to the defining class. Subclasses can't see them directly. They can interact with private state only through inherited public/protected methods.

This is intentional. It prevents subclasses from depending on internal implementation details that the parent class might change.

When NOT to Use Inheritance

A few cases where inheritance is the wrong choice:

The relationship is has-a, not is-a. Use composition.

You're only reusing code, not extending behavior. Use composition or extract a utility class.

You want to modify the parent's behavior in ways the parent didn't anticipate. This usually breaks invariants. Refactor the parent or use composition.

You're working with classes you don't control (third-party). Subclassing third-party classes is fragile — they can change in ways that break your subclass.

The parent is final. Effective Java Item 19 — many classes should be final precisely because they aren't designed for inheritance.

Most veteran Java developers use inheritance sparingly. New code often favors interfaces (Module 11) over class hierarchies. The exception: framework code where extension is the explicit design intent (Spring's controllers, JPA's entities).

A Real Production Story

At Amazon, the S3 team has a complex internal hierarchy for representing different kinds of storage operations — GetObject, PutObject, DeleteObject, ListObjects, dozens of variations.

The original design had a deep inheritance chain: Operation → RequestOperation → ObjectOperation → GetObject. Five levels deep in some cases.

Over time, this caused friction. Adding a new operation type required understanding the entire hierarchy. Changing a parent class affected every descendant. We had bugs where overridden methods in deep subclasses unexpectedly interacted with grandparent behavior.

The fix took two years: flatten the hierarchy and use composition for shared behavior. Most "operations" are now flat classes implementing interfaces, with shared logic in composable utility classes. The hierarchy is at most two levels deep now.

The lesson: deep inheritance hierarchies are expensive to maintain. Default to shallow hierarchies (zero or one level of inheritance), composition for code reuse, and interfaces for polymorphism. We'll cover all of these in this module and the next.

What This Module Covers

Module 8 covers inheritance and polymorphism in depth:

After this module, you'll be able to design appropriate inheritance hierarchies, use composition where it fits better, override methods correctly, and predict the behavior of polymorphic calls under various scenarios.

Common Pitfalls

Warning: Inheritance for code reuse alone. If the is-a relationship doesn't hold, use composition. This is the most common inheritance mistake.

Deep hierarchies. Three or more levels deep is a smell. Refactor to shallower hierarchies with composition.

Extending classes you don't control. Third-party classes can change in ways that break your subclass. Composition is more robust.

Forgetting that private members aren't inherited. Subclasses can't see private fields or methods of the parent.

Thinking inheritance is the only way to share code. Interfaces with default methods (Module 11), composition with delegation, and utility classes are all alternatives.

Why This Matters in Interviews

Interview: Inheritance questions are universal. "When would you use inheritance vs composition?" appears in nearly every senior Java interview. The expected answer covers the is-a vs has-a distinction, the brittleness of deep hierarchies, and the modern preference for composition + interfaces.

"What does inheritance NOT inherit?" tests understanding of private members and constructors.

Topic 8.14 covers the full Interview Lens for inheritance and polymorphism.

Try It

Exercise: For each scenario, decide: inheritance or composition?

  1. A SavingsAccount and CheckingAccount both have balances and can be deposited/withdrawn from
  2. A Car needs to use an Engine
  3. A Cat and Dog are both Animals
  4. A Stack needs to store items internally
  5. A JsonResponse is a kind of HttpResponse
  6. A Logger needs a FileWriter for output

Then for the inheritance cases, sketch the class hierarchy. For composition, identify what's the has-a relationship.

The answers aren't always clear-cut. Practice the judgment.

Back to Java Basic To Advance