Inheritance in Java
Master Java inheritance: extends keyword, super, method overriding, and substitutability, with practical guidance for reliable class hierarchies.
Inheritance lets a subclass reuse a parent implementation, but each subtype must still honor the promises callers rely on. This guide explains constructor calls, overriding, sealed classes, and the Liskov Substitution Principle, with examples of failures caused by fragile base classes and skipped lifecycle work. Use its checklists to decide when an inheritance hierarchy fits and when composition gives you safer flexibility.
Inheritance in Java
Introduction
Inheritance is the mechanism that lets you build class hierarchies where subclasses automatically receive fields and methods from their ancestors — code reuse through a “is-a” relationship.
When to Use
Use inheritance when:
- A true “is-a” relationship exists — a
Dogis anAnimal - Shared behavior is identical — subclasses should use the exact same implementation
- You need polymorphism — treating different types through a common supertype
- Designing for extension — allowing subclasses to customize behavior via overriding
// Parent class
public class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + " is eating");
}
public void sleep() {
System.out.println(name + " is sleeping");
}
}
// Child class
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // Call parent constructor
this.breed = breed;
}
@Override
public void eat() {
System.out.println(name + " (a " + breed + ") is eating kibble");
}
public void bark() {
System.out.println(name + " says Woof!");
}
}
When Not to Use
Avoid inheritance when:
- “Has-a” relationship — a
Carhas anEngine, not is anEngine - Different implementations for same behavior — use interfaces instead
- Unrelated classes — don’t force hierarchy where none exists
- Multiple inheritance needed — Java only supports single class inheritance, use interfaces
// Bad: forced inheritance
public class Stack extends ArrayList { // Stack is not an ArrayList!
// Inappropriate — ArrayList has too much functionality
}
// Good: composition
public class Stack {
private List<String> items = new ArrayList<>(); // Has-a relationship
}
Inheritance Hierarchy — Mermaid Diagram
classDiagram
class Animal {
+String name
+eat() void
+sleep() void
}
class Dog {
+String breed
+bark() void
+eat() void
}
class Cat {
+boolean indoor
+meow() void
+eat() void
}
class Bird {
+boolean canFly
+chirp() void
}
Animal <|-- Dog
Animal <|-- Cat
Animal <|-- Bird
note for Dog "extends — inherits name, age,\neat(), sleep() from Animal"
Failure Scenarios
1. Breaking the Liskov Substitution Principle
public class Rectangle {
protected double width;
protected double height;
public void setWidth(double w) { width = w; }
public void setHeight(double h) { height = h; }
}
public class Square extends Rectangle {
// Violation: a Square cannot independently set width and height
@Override
public void setWidth(double w) {
width = w;
height = w; // Must keep them equal
}
@Override
public void setHeight(double h) {
width = h;
height = h;
}
}
// If code expects Rectangle, Square breaks it:
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(3); // Not a square anymore! But stored width is now 3 too
2. Forgetting to Call super()
public class Parent {
public Parent(String name) {
System.out.println("Parent constructed: " + name);
}
}
public class Child extends Parent {
public Child() {
// super(); // IMPLICIT — but only if Parent has no-arg constructor
// If Parent has only Parent(String), this won't compile
}
}
3. Methods That Cannot Be Overridden
Java uses runtime dispatch for overridable instance methods, resolving the implementation from the actual object type. A subclass can replace an inherited method unless the method is final; the compiler rejects an override of a final method.
For example, Object.getClass() is final, so a subclass cannot make an object report a different runtime Class. Marking a method final also tells subclass authors that its implementation is part of the parent contract. The compiler enforces that boundary:
public class Base {
public final void finalMethod() {
System.out.println("Base behavior");
}
}
public class Child extends Base {
@Override
public void finalMethod() { // Compile error: cannot override a final method
System.out.println("Child behavior");
}
}
Production Failure Scenarios
A base class can provide logging, retries, and resource cleanup, but a subclass can lose those guarantees if it overrides the public workflow method.
class Job {
public void run() {
System.out.println("Starting job");
try {
execute();
} finally {
releaseLock();
System.out.println("Job finished");
}
}
protected void execute() {}
protected void releaseLock() {
// Release the distributed lock
}
}
class ImportJob extends Job {
@Override
public void run() {
importRows(); // Skips the parent's lock cleanup and lifecycle logging
}
private void importRows() {
// Import data
}
}
In production, the import may appear successful while the distributed lock remains held. Later jobs then stall, and operators see growing queue delays rather than an obvious failure. Keep the workflow in a final template method and expose a protected hook such as execute() for subclasses. Add a test that runs each job subtype and verifies cleanup happens after both success and exceptions.
Another warning sign is a subtype that throws UnsupportedOperationException for a method promised by its superclass. Callers typed to the parent can then fail only for certain runtime subtypes. Split the parent contract into smaller interfaces, or use composition when the subtype cannot honor the full contract.
Trade-off Table
| Inheritance Type | Code Reuse | Flexibility | Coupling |
|---|---|---|---|
Class inheritance (extends) |
Full | Moderate | Tight — subclass depends on parent |
| Interface implementation | None (just contracts) | High | Low — depends only on contract |
| Composition | Delegated | High | Low — depends on collaborator interface |
| Default method (interface) | Partial | Moderate | Moderate |
Code Snippets
Method Overriding with @Override
public class Vehicle {
protected double speed;
public void move() {
System.out.println("Vehicle moving at " + speed + " km/h");
}
public double getSpeed() {
return speed;
}
}
public class Car extends Vehicle {
private int wheels = 4;
@Override // Compiler checks this is actually overriding something
public void move() {
System.out.println("Car driving at " + speed + " km/h with " + wheels + " wheels");
}
public void honk() {
System.out.println("Car honking!");
}
}
Using super to Access Parent Members
public class Employee {
protected String name;
protected double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public void work() {
System.out.println(name + " is working");
}
public String getInfo() {
return name + " earns " + salary;
}
}
public class Manager extends Employee {
private int teamSize;
public Manager(String name, double salary, int teamSize) {
super(name, salary); // Call parent constructor
this.teamSize = teamSize;
}
@Override
public void work() {
super.work(); // Call parent's work() first
System.out.println(name + " is also managing " + teamSize + " people");
}
@Override
public String getInfo() {
return super.getInfo() + " (manages " + teamSize + ")"; // Extend parent info
}
}
Sealed Classes and Controlled Inheritance
Java 17 sealed classes let a parent name the types allowed to extend it. This is useful when a domain has a small, known set of variants and callers should handle every case explicitly.
public sealed interface Payment permits CardPayment, CashPayment {}
public final class CardPayment implements Payment {}
public final class CashPayment implements Payment {}
Each permitted subtype must declare itself final, sealed, or non-sealed. Use final when the variant should stop there; choose non-sealed only when downstream extension is part of the design. Sealed types make the hierarchy easier to reason about, but they can make plugins or independently maintained extensions harder to support.
Observability Checklist
- Inheritance only for true “is-a” relationships
-
@Overrideannotation used on all overriding methods -
super()called explicitly when parent has no default constructor - Parent methods accessible via
superwhen needed - Liskov Substitution Principle respected — subclasses work wherever parents are expected
Security Notes
- Don’t expose internal state via protected fields — use protected methods
- Validate in subclass — don’t trust parent invariants alone
- Immutable parent — if parent is immutable, subclass should be too
- Constructor security — don’t let subclass constructors bypass parent initialization
public class SecureAccount {
private final String accountId;
private double balance;
// Constructor sets required invariants
public SecureAccount(String accountId, double initialBalance) {
if (accountId == null || accountId.isEmpty()) {
throw new IllegalArgumentException("Invalid account ID");
}
this.accountId = accountId;
this.balance = initialBalance >= 0 ? initialBalance : 0;
}
}
// Subclass must call super(), which validates accountId
public class SavingsAccount extends SecureAccount {
private double interestRate;
public SavingsAccount(String accountId, double initialBalance, double interestRate) {
super(accountId, initialBalance); // Parent validation runs first
this.interestRate = interestRate;
}
}
Security and Compliance Notes
Avoid calling an overridable authorization method from a superclass operation. Java dispatches that call to the runtime subtype, so a subclass can weaken the check even when the caller uses the superclass API.
class DocumentService {
public byte[] read(User user, Document document) {
if (!canRead(user, document)) {
throw new SecurityException("Access denied");
}
return document.contents();
}
protected boolean canRead(User user, Document document) {
return document.ownerId().equals(user.id());
}
}
class SharedDocumentService extends DocumentService {
@Override
protected boolean canRead(User user, Document document) {
return true; // Accidentally makes every document readable
}
}
The symptom may be unauthorized reads that still pass through the normal service and appear in ordinary request logs. A compliance review can also find that the recorded policy differs from the policy actually applied. Keep authorization checks private or final, enforce policy in a dedicated component that cannot be overridden through inheritance, and test access through every exposed service subtype. Record the subject, resource, decision, and policy result in the audit trail without logging document contents or credentials.
Pitfalls
- Deep inheritance hierarchies — prefer shallow hierarchies (max 2-3 levels)
- Forcing inheritance where composition fits better — “has-a” vs “is-a”
- Breaking substitutability — subclass must work wherever parent is expected
- Overriding methods that weren’t designed for extension — the fragile base class problem
- Diamond inheritance — not possible in Java (use interfaces for multiple contracts)
// Common mistake: inheritance for code reuse rather than polymorphism
class MyStack extends ArrayList { // Wrong — Stack is not an ArrayList
public void push(Object item) { add(item); }
public Object pop() { return remove(size() - 1); }
}
// Correct: composition
class MyStack {
private List<Object> items = new ArrayList<>();
public void push(Object item) { items.add(item); }
public Object pop() { return items.remove(items.size() - 1); }
}
Common Pitfalls / Anti-Patterns
Extending a class only to reuse a few methods gives callers every public operation on the parent. For example, Stack extends ArrayList lets stack users insert at index zero or remove from the middle, so the type no longer guarantees last-in, first-out behavior. Prefer a stack that contains a private Deque and exposes only push, pop, and peek.
Watch for these inheritance traps:
- Subclasses that weaken parent guarantees: a
Squaresubtype cannot safely honor aRectangleAPI that allows width and height to change independently. Use separate shape types or an interface with operations both types can support. - Overrides that skip parent work: if the superclass owns validation, cleanup, auditing, or transaction boundaries, a replacement method can silently bypass it. Keep the workflow fixed and expose narrow hooks.
- Protected mutable state: subclasses can change parent data without going through its validation. Prefer private fields and protected methods with explicit invariants.
- Deep hierarchies for small variations: each level adds another place where behavior can change. Use composition or a strategy object when behavior needs to vary independently.
When a proposed subtype needs exceptions to the parent contract, treat that as a design signal. Narrow the parent API or replace the inheritance relationship before callers have to learn which subclasses behave differently.
Quick Recap
extends= inheritance keyword for classes (single inheritance only)super= reference to parent class members (constructor callsuper(), method callsuper.method())@Override= annotation ensuring method actually overrides parent method- Subclass must call
super()explicitly if parent has no default constructor - Liskov Substitution Principle = subclass objects must be usable wherever parent objects are expected
- Favor composition over inheritance = “has-a” over “is-a” for flexibility
Quick Recap Checklist
- Use
extendsonly when the subclass is a valid substitute for its parent. - Call an accessible parent constructor with
super(...)when no suitable no-argument constructor exists. - Mark overridden methods with
@Override. - Prefer composition for “has-a” relationships or replaceable behavior.
Interview Questions
super() automatically?super() call when a constructor does not begin with an explicit this(...) or super(...) call. This only compiles when the parent has an accessible no-argument constructor; otherwise, the child must call a matching parent constructor explicitly.@Override?ArrayList, since callers should not be able to use every list operation on the stack.Further Reading
- Polymorphism in Java — runtime method dispatch
- Composition over Inheritance — alternative to inheritance
- The Object Class in Java — root of all class hierarchies
- Oracle Java Tutorial: Inheritance — class inheritance and member behavior
Conclusion
Inheritance models “is-a” relationships — a Dog is an Animal, so Dog can inherit from Animal. The child class automatically receives the parent’s fields and methods, gaining reusable behavior without manual implementation. The extends keyword establishes this relationship, and super() calls ensure parent initialization runs before child-specific setup.
The inheritance hierarchy diagram shows how subclasses form a tree rooted at a common ancestor. At the top of every Java class hierarchy sits Object, which provides fundamental methods like toString(), equals(), and hashCode() (detailed in The Object Class in Java). Understanding this chain helps explain why all objects share certain behaviors.
Method overriding lets subclasses replace parent implementations with their own. The @Override annotation is not optional — it is the compiler’s sanity check that you are actually overriding something, catching typos in method names before they become runtime surprises. The @Override annotation combined with the Liskov Substitution Principle ensures subclasses honor their parent’s contract: anywhere an Animal is expected, a Dog must work.
The trade-off table reveals why composition (detailed in Composition over Inheritance) is often preferred. Inheritance creates tight coupling — the subclass is bound to the parent’s implementation details, which can change unexpectedly. When inheritance truly models an “is-a” relationship with a stable parent class, it is appropriate; but for flexible designs that need runtime behavior swapping, composition is the better tool.
Inheritance interacts with polymorphism (explored in Polymorphism in Java) — subclass instances can be stored in parent-type variables, and the correct method gets called based on the actual object type at runtime, not the reference type. This is the mechanism that makes interfaces and inheritance hierarchies powerful for writing flexible, extensible code.
Summary
Use inheritance when a stable “is-a” relationship supports substitutability and shared behavior. Keep parent contracts clear, use @Override to catch mistakes, and prefer composition when behavior needs to vary independently or inheritance would create tight coupling.
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.