Home Java Java Inheritance Explained — How, Why and When to Use It

Java Inheritance Explained — How, Why and When to Use It

In Plain English 🔥
Think of a smartphone. Every smartphone — whether it's a Samsung, an iPhone, or a Pixel — has things in common: a screen, a battery, and the ability to make calls. Instead of redesigning those parts from scratch for every brand, manufacturers start with a 'base phone' blueprint and then add their own unique features on top. In Java, inheritance works exactly like that. You write a base class once, and every other class that 'inherits' from it automatically gets all its features — no copy-paste required.
⚡ Quick Answer
Think of a smartphone. Every smartphone — whether it's a Samsung, an iPhone, or a Pixel — has things in common: a screen, a battery, and the ability to make calls. Instead of redesigning those parts from scratch for every brand, manufacturers start with a 'base phone' blueprint and then add their own unique features on top. In Java, inheritance works exactly like that. You write a base class once, and every other class that 'inherits' from it automatically gets all its features — no copy-paste required.

Every production Java codebase you'll ever work in uses inheritance. It's not an academic exercise — it's the mechanism that lets a PaymentProcessor class share core logic with CreditCardProcessor and PayPalProcessor without duplicating a single line. When a bug is fixed in the base class, every subclass benefits instantly. That's not just convenient, it's the kind of thing that separates maintainable software from a tangled mess of duplicated code.

The problem inheritance solves is code duplication across related types. Imagine you're building an e-commerce platform. You have products: Books, Electronics, and Clothing. Every product has a name, a price, and a method to display itself. Without inheritance, you'd write those fields and methods three times, then fix bugs three times, and explain the logic to three different future teammates. Inheritance lets you define shared behaviour exactly once in a parent class and let child classes focus only on what makes them different.

By the end of this article you'll understand not just how to write the extends keyword, but why Java was designed this way, when inheritance is the right tool versus when it isn't, how method overriding actually works under the hood, and the patterns senior engineers use in real projects. You'll also know the mistakes that trip up even experienced developers — and how to avoid them.

What Inheritance Actually Does — The extends Keyword Unpacked

When one class extends another, the child class inherits every non-private field and method from the parent. That means the child class can use them as if it had written them itself. The parent is often called the superclass or base class; the child is called the subclass or derived class.

Java uses single inheritance for classes — each class can only extend one parent. This is a deliberate design choice to avoid the 'Diamond Problem' (where two parents both define the same method and the child doesn't know which one to use). If you need behaviour from multiple sources, Java's answer is interfaces — but that's a separate topic.

Here's the critical insight most tutorials skip: inheritance models an IS-A relationship. A Dog IS-A Animal. A SavingsAccount IS-A BankAccount. If you can't make that sentence sound natural, you probably shouldn't be using inheritance — you should be using composition instead. Inheritance locks in a tight relationship between two classes, so getting the design right from the start saves you painful refactors later.

BankAccountDemo.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
// A realistic banking example showing why inheritance saves us from duplication

// PARENT CLASS — defines everything ALL bank accounts share
class BankAccount {
    private String accountHolder;  // private: only accessible within this class
    protected double balance;      // protected: accessible to subclasses

    public BankAccount(String accountHolder, double initialBalance) {
        this.accountHolder = accountHolder;
        this.balance = initialBalance;
    }

    // Every account type can deposit money — logic lives here ONCE
    public void deposit(double amount) {
        if (amount <= 0) {
            System.out.println("Deposit amount must be positive.");
            return;
        }
        balance += amount;
        System.out.println(accountHolder + " deposited $" + amount + ". New balance: $" + balance);
    }

    public double getBalance() {
        return balance;
    }

    public String getAccountHolder() {
        return accountHolder;
    }

    public void printSummary() {
        System.out.println("Account: " + accountHolder + " | Balance: $" + balance);
    }
}

// CHILD CLASS — inherits everything from BankAccount, adds its own rules
class SavingsAccount extends BankAccount {
    private double interestRate;  // unique to SavingsAccount

    public SavingsAccount(String accountHolder, double initialBalance, double interestRate) {
        // super() calls the PARENT constructor — must be the first line
        super(accountHolder, initialBalance);
        this.interestRate = interestRate;
    }

    // Behaviour unique to savings accounts — the parent has no concept of this
    public void applyMonthlyInterest() {
        double interest = balance * interestRate;
        balance += interest;  // 'balance' is accessible because it's protected in parent
        System.out.println("Interest of $" + String.format("%.2f", interest) + " applied. New balance: $" + String.format("%.2f", balance));
    }
}

// ANOTHER CHILD CLASS — same parent, different specialisation
class CheckingAccount extends BankAccount {
    private double overdraftLimit;

    public CheckingAccount(String accountHolder, double initialBalance, double overdraftLimit) {
        super(accountHolder, initialBalance);
        this.overdraftLimit = overdraftLimit;
    }

    // Checking accounts allow withdrawal up to the overdraft limit
    public void withdraw(double amount) {
        if (amount > balance + overdraftLimit) {
            System.out.println("Withdrawal denied: exceeds overdraft limit.");
        } else {
            balance -= amount;
            System.out.println(getAccountHolder() + " withdrew $" + amount + ". Balance: $" + String.format("%.2f", balance));
        }
    }
}

public class BankAccountDemo {
    public static void main(String[] args) {
        SavingsAccount savings = new SavingsAccount("Alice", 1000.00, 0.03);
        savings.deposit(500.00);         // inherited from BankAccount — no duplication
        savings.applyMonthlyInterest();  // unique to SavingsAccount
        savings.printSummary();          // inherited from BankAccount

        System.out.println("---");

        CheckingAccount checking = new CheckingAccount("Bob", 200.00, 150.00);
        checking.deposit(100.00);        // same deposit() method, reused via inheritance
        checking.withdraw(400.00);       // unique to CheckingAccount
        checking.printSummary();         // inherited from BankAccount
    }
}
▶ Output
Alice deposited $500.0. New balance: $1500.0
Interest of $45.00 applied. New balance: $1545.00
Account: Alice | Balance: $1545.0
---
Bob deposited $100.0. New balance: $300.0
Bob withdrew $400.0. Balance: $-100.00
Account: Bob | Balance: $-100.0
⚠️
Pro Tip: protected vs private in Parent ClassesUse `protected` for fields you *intend* subclasses to access directly (like `balance` above). Use `private` for fields that should only be touched through getter/setter methods, even by subclasses. Defaulting everything to `protected` is a common shortcut that leaks internal state — be deliberate about it.

Method Overriding — Giving Inherited Behaviour Your Own Spin

Inheritance lets you reuse a method, but overriding lets you replace it with a better version specific to the child class. This is where inheritance really earns its keep in real projects.

To override a method, the child class defines a method with the exact same name, return type, and parameter list as the parent. Java's @Override annotation isn't strictly required — but you should always use it. It tells the compiler 'I intend to override a parent method here.' If you make a typo in the method name and it doesn't match the parent, the compiler will catch it and throw an error. Without @Override, it silently creates a new method instead, which is a nasty bug to track down.

The super keyword is your escape hatch. Inside an overriding method, super.methodName() calls the parent's original version. This is incredibly useful when you want to extend the parent's behaviour rather than completely replace it — think logging, validation, or adding a pre-step before the parent's core logic runs.

Under the hood, this works through dynamic dispatch: when you call a method on an object, Java looks at the actual runtime type of the object — not the declared type — to decide which version to run. This is the foundation of polymorphism.

ShippingCalculatorDemo.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// Real-world example: a shipping system where each carrier calculates cost differently

class ShippingCarrier {
    protected String carrierName;
    protected double baseRatePerKg;

    public ShippingCarrier(String carrierName, double baseRatePerKg) {
        this.carrierName = carrierName;
        this.baseRatePerKg = baseRatePerKg;
    }

    // Default shipping cost — child classes will override this
    public double calculateShippingCost(double weightKg, double distanceKm) {
        return baseRatePerKg * weightKg;
    }

    public void printQuote(double weightKg, double distanceKm) {
        // Calls calculateShippingCost() — at runtime, Java runs the CHILD's version
        // if this object is actually a child instance (dynamic dispatch in action)
        double cost = calculateShippingCost(weightKg, distanceKm);
        System.out.println(carrierName + " quote: $" + String.format("%.2f", cost));
    }
}

class ExpressCarrier extends ShippingCarrier {
    private double expressMultiplier;

    public ExpressCarrier(String carrierName, double baseRatePerKg, double expressMultiplier) {
        super(carrierName, baseRatePerKg);
        this.expressMultiplier = expressMultiplier;
    }

    @Override  // Always use this — protects you from typo bugs
    public double calculateShippingCost(double weightKg, double distanceKm) {
        // Call the parent's calculation first, then add the express premium
        double baseCost = super.calculateShippingCost(weightKg, distanceKm);
        return baseCost * expressMultiplier;
    }
}

class FreightCarrier extends ShippingCarrier {
    private double costPerKm;

    public FreightCarrier(String carrierName, double baseRatePerKg, double costPerKm) {
        super(carrierName, baseRatePerKg);
        this.costPerKm = costPerKm;
    }

    @Override
    public double calculateShippingCost(double weightKg, double distanceKm) {
        // Freight pricing is completely different — weight AND distance matter
        double weightCost = baseRatePerKg * weightKg;
        double distanceCost = costPerKm * distanceKm;
        return weightCost + distanceCost;
    }
}

public class ShippingCalculatorDemo {
    public static void main(String[] args) {
        double packageWeight = 5.0;   // kg
        double shippingDistance = 200; // km

        // Declared as ShippingCarrier, but each object is a different subtype
        // printQuote() calls the CORRECT calculateShippingCost() for each — that's polymorphism
        ShippingCarrier standardCarrier = new ShippingCarrier("PostCo Standard", 2.50);
        ShippingCarrier expressCarrier  = new ExpressCarrier("QuickShip Express", 2.50, 2.0);
        ShippingCarrier freightCarrier  = new FreightCarrier("HeavyHaul Freight", 1.80, 0.05);

        standardCarrier.printQuote(packageWeight, shippingDistance);
        expressCarrier.printQuote(packageWeight, shippingDistance);  // runs ExpressCarrier's override
        freightCarrier.printQuote(packageWeight, shippingDistance);  // runs FreightCarrier's override
    }
}
▶ Output
PostCo Standard quote: $12.50
QuickShip Express quote: $25.00
HeavyHaul Freight quote: $19.00
⚠️
Watch Out: Forgetting @Override Costs You HoursIf you write `public double calculateshippingCost(...)` (lowercase 's') without `@Override`, Java won't error — it'll quietly create a second, separate method. Your parent's `printQuote()` will keep calling the original version and you'll spend an afternoon confused about why your override has no effect. Always use `@Override`. Always.

The super Keyword and Constructor Chaining — What Really Happens at Object Creation

When you create a child class object, Java doesn't just run the child's constructor. It runs the parent's constructor first. Every time. This guarantees the parent's portion of the object is fully set up before the child tries to build on top of it.

If you explicitly call super(...) with arguments, it forwards those arguments to the matching parent constructor. If you don't call super() at all, Java silently inserts a call to the parent's no-argument constructor. If that no-argument constructor doesn't exist in the parent, you get a compile error — which confuses a lot of people the first time they see it.

super also lets you call parent methods (not just constructors) from inside the child. This is the key to the 'extend, don't replace' pattern: call super.someMethod() to run the parent's logic, then add your own lines after it. This pattern is everywhere in Android development, Spring Framework lifecycle methods, and GUI toolkits.

One hard rule you must know: super() must be the first statement in a constructor. You can't run any other code, set any fields, or do any checks before calling it. This is enforced by the compiler, not just a convention.

EmployeeHierarchyDemo.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
// Demonstrates constructor chaining and super method calls in a real HR system

class Employee {
    private String fullName;
    private String employeeId;
    private double annualSalary;

    public Employee(String fullName, String employeeId, double annualSalary) {
        this.fullName = fullName;
        this.employeeId = employeeId;
        this.annualSalary = annualSalary;
        System.out.println("[Employee constructor] Creating employee: " + fullName);
    }

    public double calculateAnnualBonus() {
        // Base employees get 5% of salary as bonus
        return annualSalary * 0.05;
    }

    public void printCompensationReport() {
        System.out.println("--- Compensation Report ---");
        System.out.println("Name:       " + fullName);
        System.out.println("ID:         " + employeeId);
        System.out.println("Salary:     $" + String.format("%.2f", annualSalary));
        System.out.println("Bonus:      $" + String.format("%.2f", calculateAnnualBonus()));
    }

    public double getAnnualSalary() {
        return annualSalary;
    }
}

class SeniorEngineer extends Employee {
    private int yearsOfExperience;
    private String specialisation;

    public SeniorEngineer(String fullName, String employeeId, double annualSalary,
                          int yearsOfExperience, String specialisation) {
        // super() MUST be first — sets up the Employee part of this object
        super(fullName, employeeId, annualSalary);
        this.yearsOfExperience = yearsOfExperience;
        this.specialisation = specialisation;
        System.out.println("[SeniorEngineer constructor] Adding specialisation: " + specialisation);
    }

    @Override
    public double calculateAnnualBonus() {
        // Start with what the parent would give, then add extra for experience
        double baseBonus = super.calculateAnnualBonus();  // calls Employee's 5% logic
        double experienceBonus = yearsOfExperience * 500.0; // $500 per year of experience
        return baseBonus + experienceBonus;
    }

    @Override
    public void printCompensationReport() {
        super.printCompensationReport();  // Print everything the parent prints first
        // Then append child-specific information
        System.out.println("Specialisation: " + specialisation);
        System.out.println("Experience: " + yearsOfExperience + " years");
    }
}

public class EmployeeHierarchyDemo {
    public static void main(String[] args) {
        System.out.println("=== Creating a SeniorEngineer ===");
        // Watch the order of constructor calls in the output
        SeniorEngineer lead = new SeniorEngineer(
            "Maya Chen", "ENG-0042", 120000.00, 8, "Distributed Systems"
        );

        System.out.println();
        lead.printCompensationReport();
    }
}
▶ Output
=== Creating a SeniorEngineer ===
[Employee constructor] Creating employee: Maya Chen
[SeniorEngineer constructor] Adding specialisation: Distributed Systems

--- Compensation Report ---
Name: Maya Chen
ID: ENG-0042
Salary: $120000.00
Bonus: $10000.00
Specialisation: Distributed Systems
Experience: 8 years
🔥
Interview Gold: Why Does the Parent Constructor Run First?Java guarantees the parent portion of an object is fully initialised before the child can use it. If the child constructor ran first and tried to call an inherited method before the parent was set up, you'd get partially-constructed objects and unpredictable behaviour. This ordering is why `super()` must be the first statement — the compiler enforces what good design demands.

When NOT to Use Inheritance — Composition vs Inheritance in the Real World

Inheritance is powerful, but it's one of the most overused patterns in Java. Senior engineers know that composition is often the better choice — and knowing the difference is what separates intermediate from advanced thinking.

The rule of thumb is the IS-A vs HAS-A test. A Car IS-A Vehicle — inheritance makes sense. A Car HAS-A Engine — that's composition; you don't extend Engine, you hold a reference to one. When you get this wrong, you end up with brittle class hierarchies where changing a parent class breaks child classes in unexpected ways (this is called the 'fragile base class' problem).

Another red flag is when you're extending a class just to reuse its methods, not because the child truly is a kind of that parent. Stack in the Java standard library infamously extends Vector — a Stack IS-NOT-A Vector, but Java's designers made that call for code reuse. The result? You can call get(index) on a Stack, which makes no semantic sense for a stack data structure. That's what bad inheritance looks like in production.

Use inheritance when you have a genuine IS-A relationship AND you want polymorphic behaviour (treating different subtypes through a shared parent type). Otherwise, default to composition.

CompositionVsInheritanceDemo.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
// Demonstrates the right and wrong way to model a 'NotificationService'

// ============================================================
// WRONG APPROACH: Extending EmailSender just to reuse its methods
// A NotificationService IS-NOT-A EmailSender — it just USES one
// ============================================================
class EmailSender {
    public void sendEmail(String recipient, String subject, String body) {
        System.out.println("Sending email to " + recipient + " | Subject: " + subject);
    }
}

// BAD: This locks NotificationService into always being an EmailSender
// You can never easily swap in a different sender, and the public API
// of NotificationService now leaks all of EmailSender's methods
class NotificationServiceBad extends EmailSender {
    public void notifyUser(String userEmail, String message) {
        // Calling the inherited method — but this is NOT an IS-A relationship
        sendEmail(userEmail, "Notification", message);
    }
}

// ============================================================
// RIGHT APPROACH: Composition — NotificationService HAS-A sender
// ============================================================
interface MessageSender {
    // An interface means we can swap the implementation at any time
    void send(String recipient, String message);
}

class EmailMessageSender implements MessageSender {
    @Override
    public void send(String recipient, String message) {
        System.out.println("[Email] To: " + recipient + " | " + message);
    }
}

class SmsMessageSender implements MessageSender {
    @Override
    public void send(String recipient, String message) {
        System.out.println("[SMS] To: " + recipient + " | " + message);
    }
}

// GOOD: NotificationService holds a reference to any MessageSender
// Swapping from Email to SMS requires zero changes to this class
class NotificationServiceGood {
    private MessageSender messageSender;  // HAS-A relationship

    // Inject the sender — this is the Dependency Injection pattern
    public NotificationServiceGood(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    public void notifyUser(String recipient, String alertMessage) {
        messageSender.send(recipient, alertMessage);
    }
}

public class CompositionVsInheritanceDemo {
    public static void main(String[] args) {
        // BAD approach — tightly coupled to EmailSender
        NotificationServiceBad badService = new NotificationServiceBad();
        badService.notifyUser("alice@example.com", "Your order has shipped.");

        System.out.println("---");

        // GOOD approach — swap the sender without touching NotificationServiceGood
        NotificationServiceGood emailNotifier = new NotificationServiceGood(new EmailMessageSender());
        NotificationServiceGood smsNotifier   = new NotificationServiceGood(new SmsMessageSender());

        emailNotifier.notifyUser("alice@example.com", "Your order has shipped.");
        smsNotifier.notifyUser("+1-555-0199",         "Your order has shipped.");
    }
}
▶ Output
Sending email to alice@example.com | Subject: Notification
---
[Email] To: alice@example.com | Your order has shipped.
[SMS] To: +1-555-0199 | Your order has shipped.
⚠️
Pro Tip: The Design Rule Senior Engineers Live By'Favour composition over inheritance' is from the Gang of Four's Design Patterns book (1994) and it's still the most misunderstood advice in Java. It doesn't mean 'never use inheritance' — it means use inheritance only when a genuine IS-A relationship exists. If your main motivation is code reuse, reach for composition and interfaces first.
AspectInheritance (extends)Composition (has-a reference)
Relationship typeIS-A — Dog is an AnimalHAS-A — Car has an Engine
Code reuse mechanismAutomatic via parent classExplicit via delegation to held object
Coupling levelTight — child depends on parent internalsLoose — depends only on an interface
Flexibility at runtimeFixed — can't change parent at runtimeHigh — can swap the held object anytime
Polymorphism supportYes — via method overriding and upcastingYes — via interface implementations
Risk of misuseHigh — easy to model wrong IS-A relationshipsLow — HAS-A is almost always correct
Real-world exampleSavingsAccount extends BankAccountNotificationService uses a MessageSender
When to prefer itYou need polymorphic substitution across typesYou need flexibility or just code reuse

🎯 Key Takeaways

  • Always apply the IS-A test before using extends — if 'ChildClass IS-A ParentClass' sounds wrong in plain English, you're probably modelling a HAS-A relationship and composition is the right tool.
  • Always annotate overriding methods with @Override — it's not optional, it's your safety net against silent method-hiding bugs that are brutally hard to debug.
  • super() must be the first line in a child constructor, and Java will always run the parent constructor before the child's — this guarantees the parent portion of your object is initialised before the child touches it.
  • Inheritance enables polymorphism through dynamic dispatch: when you call an overridden method, Java checks the actual runtime type of the object — not the declared type — to decide which version to execute.

⚠ Common Mistakes to Avoid

  • Mistake 1: Calling super() as a non-first statement in a constructor — The compiler throws 'call to super must be first statement in constructor' — Move super() to be the absolute first line. You cannot run any logic, initialise any fields, or add any validation before calling the parent constructor.
  • Mistake 2: Skipping @Override and getting silent method hiding instead of overriding — If your method signature doesn't exactly match the parent (a typo, a different return type, extra parameter), Java silently creates a brand-new method rather than overriding, so polymorphism doesn't work. Fix it by always adding @Override — the compiler will immediately tell you if nothing is being overridden.
  • Mistake 3: Modelling a HAS-A relationship with inheritance — Symptom: your child class inherits methods that make no semantic sense (like a Stack that lets you call get(index)), or changing the parent class breaks unrelated child classes. Fix it by applying the IS-A test: if you can't say 'Child genuinely IS-A Parent in all contexts', switch to composition — hold a reference instead of extending.

Interview Questions on This Topic

  • QWhat is the difference between method overriding and method overloading in Java, and why does only overriding support polymorphism?
  • QCan a constructor be inherited in Java? If not, how does the parent constructor get invoked when you create a child class object?
  • QA class called Rectangle extends Shape, and Shape has a method draw(). If you store a Rectangle object in a Shape variable and call draw(), which version runs — Shape's or Rectangle's? Why? What if draw() were declared as private in Shape?

Frequently Asked Questions

Can a Java class inherit from multiple classes at the same time?

No. Java only supports single inheritance for classes — a class can extend exactly one parent. This was a deliberate design decision to avoid the Diamond Problem, where two parents both define the same method and the child is ambiguous about which one to call. If you need behaviour from multiple sources, use interfaces, which do support multiple implementation.

What is the difference between extends and implements in Java?

extends is used to inherit from another class — you get its fields and method implementations. implements is used to adopt an interface — you're promising to provide your own implementation of the interface's methods. A class can only extend one class but can implement multiple interfaces.

If a parent class has a private field, can the child class access it directly?

No. Private members are invisible outside the class they're defined in — even to subclasses. The child can still interact with that data indirectly through public or protected getter and setter methods provided by the parent. If you want a field to be accessible to subclasses but hidden from the outside world, use the protected access modifier instead.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousConstructors in JavaNext →Polymorphism in Java
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged