RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
EasyJava#178 min readJul 11, 2026

Explain Inheritance.

practice
learning
Practice modeTest yourself instead of reading straight through

Inheritance is one of the fastest ways to see whether a class truly belongs in the same family.

Question: Explain inheritance in Java.

Answer: Inheritance lets one class reuse fields and methods from another class using extends. The child class is called a subclass, and the parent is called a superclass. In Java, it models an is-a relationship, such as SavingsAccount is an Account. It also supports polymorphism, which means a superclass reference can point to a subclass object.

Interview-Ready Answer: Inheritance in Java is when one class extends another class and automatically gets its accessible state and behavior. I use it when the subclass really is a kind of the parent class, like Dog extends Animal. It helps reuse code and enables polymorphism, so I can treat different subclasses through one superclass reference. One important detail is that constructors are not inherited, and Java allows only single class inheritance, so I still need to use super(...) to build the parent part correctly.

🧠 Memory Map
Memory map — visual summary of this topic

What inheritance really means

Detailed Explanation: Think of inheritance as a promise: every subclass should be a valid version of its superclass. If code works with the parent type, it should also work with the child type without surprises. That is why inheritance is more than code reuse; it is a type relationship. In Java, a class can extend only one other class, but it can implement many interfaces, so inheritance is the main way to share a concrete base class.

How it works under the hood

  1. You declare a superclass with shared data and behavior, such as common fields and methods.
  2. A subclass writes extends ParentClass. The compiler checks that it is allowed to inherit from that class; for example, a final class cannot be extended.
  3. When you create the subclass object, Java runs the superclass constructor first by calling super(...). This is why the parent part of the object is built before the child part.
  4. The actual object contains all inherited instance fields plus its own fields. Methods are not copied into each object; they live with the class definition, which keeps object memory smaller.
  5. When you call an instance method, Java uses dynamic dispatch, meaning the runtime type decides which overridden method runs. If the method is not overridden, the inherited version runs.
  6. The reference type still matters at compile time. A variable typed as Account can call only members declared in Account, even if it points to a SavingsAccount object.

When and why to use it

  • Use inheritance when the relationship is truly is-a: a Circle is a Shape, a Dog is an Animal.
  • Use it when subclasses share a stable contract and only differ in a few steps of behavior.
  • Use it when a framework expects you to override template methods, such as a base class that defines the flow and child classes fill in details.

Inheritance vs composition

Composition means a class contains another object and uses it, which is a has-a relationship. Interviewers love this comparison because many bad designs use inheritance when composition would be safer.

AspectInheritanceComposition
Relationshipis-ahas-a
CouplingTighterLooser
FlexibilityLowerHigher
Best forShared type familyReusable parts

Important edge cases

  • Constructors are not inherited. If the superclass has no no-arg constructor, the subclass must call a specific super(...) constructor.
  • Private members exist inside the object, but the subclass cannot access them directly. Use protected methods or getters if the child needs them.
  • Static methods are not overridden in the same way as instance methods; they are hidden, not polymorphic.
  • You cannot extend more than one class in Java, so inheritance should be used sparingly.
  • Access cannot become more restrictive when overriding. For example, a public method cannot be overridden as protected.

Performance note: Inheritance itself does not create a special runtime penalty. Overridden method calls are typically constant time, and each object stores its fields once. Exact object size depends on the JVM, but a typical HotSpot object has a header of roughly 12 to 16 bytes plus its fields and alignment. The real cost is usually design complexity, not CPU time.

Real-World Example: Imagine a checkout service in an e-commerce system. It has a PaymentMethod base class and subclasses like CardPayment, WalletPayment, and BankTransfer. The base class defines shared behavior such as validation and receipt formatting, while each subclass implements the payment-specific steps. This lets the checkout flow store them in one list and process them through the same API.

Now the bug: a team once created GiftCardPayment extends CardPayment just because both sounded like cards. During a holiday sale, refund requests for gift cards started failing because the card-specific authorization path ran against gift cards, which do not have PAN numbers or card network checks. Logs showed errors like IllegalStateException: card authorization required, support tickets spiked, and customers saw refunds stuck in pending for hours. The root cause was using inheritance for convenience instead of a true is-a relationship.

The fix was to move the shared receipt and amount formatting into a helper and make GiftCardPayment extend PaymentMethod directly. That reduced fake fields, removed risky casts, and made the code match the business model.

Java
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

// The base class owns the shared rules so every subtype validates money the same way.
abstract class Account {
    private final String owner;
    private double balance;

    protected Account(String owner, double openingBalance) {
        if (owner == null || owner.isBlank()) {
            throw new IllegalArgumentException("Owner name must not be blank");
        }
        if (openingBalance < 0) {
            throw new IllegalArgumentException("Opening balance cannot be negative");
        }
        this.owner = owner;
        this.balance = openingBalance;
    }

    public String getOwner() {
        return owner;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit amount must be positive");
        }
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Withdraw amount must be positive");
        }
        if (amount > balance) {
            throw new IllegalArgumentException("Insufficient funds");
        }
        balance -= amount;
    }

    public String summary() {
        return getClass().getSimpleName() + "{owner=" + owner + ", balance=" + money(balance) + "}";
    }

    protected String money(double amount) {
        return String.format(Locale.US, "%.2f", amount);
    }
}

class SavingsAccount extends Account {
    private final double annualInterestRate; // 0.12 means 12% per year

    public SavingsAccount(String owner, double openingBalance, double annualInterestRate) {
        super(owner, openingBalance);
        if (annualInterestRate < 0) {
            throw new IllegalArgumentException("Interest rate cannot be negative");
        }
        this.annualInterestRate = annualInterestRate;
    }

    public void addMonthlyInterest() {
        // The subclass adds its own rule, but it still reuses the inherited deposit logic.
        deposit(getBalance() * annualInterestRate / 12.0);
    }

    @Override
    public String summary() {
        return super.summary() + ", annualInterestRate=" + money(annualInterestRate * 100) + "%";
    }
}

class PremiumSavingsAccount extends SavingsAccount {
    private final double monthlyBonus;

    public PremiumSavingsAccount(String owner, double openingBalance, double annualInterestRate, double monthlyBonus) {
        super(owner, openingBalance, annualInterestRate);
        if (monthlyBonus < 0) {
            throw new IllegalArgumentException("Monthly bonus cannot be negative");
        }
        this.monthlyBonus = monthlyBonus;
    }

    @Override
    public void addMonthlyInterest() {
        // Overriding lets the child specialize behavior while keeping the same public API.
        super.addMonthlyInterest();
        deposit(monthlyBonus);
    }

    @Override
    public String summary() {
        return super.summary() + ", monthlyBonus=" + money(monthlyBonus);
    }
}

public class InheritanceDemo {
    public static void main(String[] args) {
        List<Account> accounts = new ArrayList<>();
        accounts.add(new SavingsAccount("Asha", 1000, 0.12));
        accounts.add(new PremiumSavingsAccount("Ravi", 1500, 0.06, 5));

        for (Account account : accounts) {
            System.out.println("Before: " + account.summary());
            account.withdraw(100); // Polymorphism: the same method call works for any Account subtype.
            System.out.println("After withdrawal: " + account.summary());

            // The reference type is Account, so we only call subtype-specific behavior after checking the real type.
            if (account instanceof SavingsAccount) {
                ((SavingsAccount) account).addMonthlyInterest();
                System.out.println("After interest: " + account.summary());
            }
            System.out.println();
        }

        Account reference = new SavingsAccount("Mina", 200, 0.24);
        reference.deposit(50); // Inherited behavior is available through the parent type.
        System.out.println("Superclass reference: " + reference.summary());

        try {
            new SavingsAccount("Bad", -10, 0.05);
        } catch (IllegalArgumentException ex) {
            System.out.println("Rejected invalid account: " + ex.getMessage());
        }

        try {
            reference.withdraw(10_000);
        } catch (IllegalArgumentException ex) {
            System.out.println("Rejected invalid withdrawal: " + ex.getMessage());
        }
    }
}

Follow-up & Tricky Questions:

  • How is inheritance different from composition? Inheritance models is-a, while composition models has-a. If you are only reusing code, composition is often safer because it keeps classes less tightly coupled.
  • What is method overriding? Overriding means a subclass provides its own version of a superclass instance method with the same signature. The runtime chooses the subclass version when the object is actually a subclass instance.
  • Can a Java class extend multiple classes? No. Java supports single class inheritance, but a class can implement multiple interfaces to gain multiple contracts.
  • Why do we call super(...)? It initializes the parent state first. If the parent has no no-arg constructor, this call is mandatory and the code will not compile without it.
  • What does protected help with in inheritance? It gives controlled access to subclasses and same-package classes. It is useful when a child needs internal helper methods without exposing them publicly.
  • Are constructors inherited? No, constructors are not inherited. A subclass must define its own constructor and chain to a superclass constructor as needed.
  • Are static methods overridden? No. Static methods belong to the class, not the object, so they are hidden rather than polymorphically overridden.
  • Can a subclass make an overridden method more restrictive? No. The access level must stay the same or become less restrictive, such as protected to public.
  • Does a subclass get private fields and methods? The data is still part of the object, but the subclass cannot access private members directly. You use superclass methods to interact with them.
  • Is every superclass a good inheritance candidate? No. If the relationship is not truly is-a, the design will be fragile. In that case, prefer composition or interfaces.
  • Does inheritance copy code into every object? No. The object stores its fields; methods are shared through the class definition. That is why inheritance is about type structure, not per-object duplication.

Tricky: Can I inherit just to avoid writing duplicate code? Not safely. Duplicate code is a symptom, but the fix must still match the real domain relationship; otherwise you create a misleading hierarchy.

Tricky: If I override equals in a child, do I also need to think about hashCode? Yes. If two objects are equal, they must have the same hash code, so both methods should follow the same rules even in an inheritance hierarchy.

Tricky: Does polymorphism work for fields the same way it works for methods? No. Method dispatch is polymorphic, but field access is based on the reference type, which is a common source of confusion.

Common Mistakes

  • Using inheritance for code reuse only. Correction: use inheritance only when the child is truly a kind of the parent; otherwise choose composition.
  • Forgetting constructor chaining. Correction: if the superclass needs arguments, the subclass constructor must call super(...).
  • Thinking private members are directly inherited. Correction: they belong to the object, but only superclass code can access them directly.
  • Confusing overriding with overloading. Correction: overriding changes a parent method in a subclass; overloading means same method name with different parameter lists.

Memory Hook

Memory Hook: Use the family-tree rule: if the child can honestly say I am a kind of parent, inherit; if it just has a part, compose. A dog is an animal, but a car has an engine.

Cheat Sheet

  • extends connects a subclass to a superclass.
  • Java supports one class parent, many interfaces.
  • Constructors are not inherited; super() runs first.
  • Overridden methods are chosen by the runtime object type.
  • Use inheritance for true is-a relationships.
  • Prefer composition when you only need reuse or flexibility.

Practice Tasks

  • Build Animal, Dog, and Cat with one overridden method such as speak().
  • Add validation to a superclass constructor, then test one valid and one invalid subclass object.
  • Take a bad has-a example and refactor it from inheritance to composition.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

import java.util.ArrayList; import java.util.List; import java.util.Locale; // The base class owns the shared rules so every subtype validates money the same way. abstract class Account { private final String owner; private double balance; protected Account(String owner, double openingBalance) { if (owner == null || owner.isBlank()) { throw new IllegalArgumentException("Owner name must not be blank"); } if (openingBalance < 0) { throw new IllegalArgumentException("Opening balance cannot be negative"); } this.owner = owner; this.balance = openingBalance; } public String getOwner() { return owner; } public double getBalance() { return balance; } public void deposit(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Deposit amount must be positive"); } balance += amount; } public void withdraw(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Withdraw amount must be positive"); } if (amount > balance) { throw new IllegalArgumentException("Insufficient funds"); } balance -= amount; } public String summary() { return getClass().getSimpleName() + "{owner=" + owner + ", balance=" + money(balance) + "}"; } protected String money(double amount) { return String.format(Locale.US, "%.2f", amount); } } class SavingsAccount extends Account { private final double annualInterestRate; // 0.12 means 12% per year public SavingsAccount(String owner, double openingBalance, double annualInterestRate) { super(owner, openingBalance); if (annualInterestRate < 0) { throw new IllegalArgumentException("Interest rate cannot be negative"); } this.annualInterestRate = annualInterestRate; } public void addMonthlyInterest() { // The subclass adds its own rule, but it still reuses the inherited deposit logic. deposit(getBalance() * annualInterestRate / 12.0); } @Override public String summary() { return super.summary() + ", annualInterestRate=" + money(annualInterestRate * 100) + "%"; } } class PremiumSavingsAccount extends SavingsAccount { private final double monthlyBonus; public PremiumSavingsAccount(String owner, double openingBalance, double annualInterestRate, double monthlyBonus) { super(owner, openingBalance, annualInterestRate); if (monthlyBonus < 0) { throw new IllegalArgumentException("Monthly bonus cannot be negative"); } this.monthlyBonus = monthlyBonus; } @Override public void addMonthlyInterest() { // Overriding lets the child specialize behavior while keeping the same public API. super.addMonthlyInterest(); deposit(monthlyBonus); } @Override public String summary() { return super.summary() + ", monthlyBonus=" + money(monthlyBonus); } } public class InheritanceDemo { public static void main(String[] args) { List<Account> accounts = new ArrayList<>(); accounts.add(new SavingsAccount("Asha", 1000, 0.12)); accounts.add(new PremiumSavingsAccount("Ravi", 1500, 0.06, 5)); for (Account account : accounts) { System.out.println("Before: " + account.summary()); account.withdraw(100); // Polymorphism: the same method call works for any Account subtype. System.out.println("After withdrawal: " + account.summary()); // The reference type is Account, so we only call subtype-specific behavior after checking the real type. if (account instanceof SavingsAccount) { ((SavingsAccount) account).addMonthlyInterest(); System.out.println("After interest: " + account.summary()); } System.out.println(); } Account reference = new SavingsAccount("Mina", 200, 0.24); reference.deposit(50); // Inherited behavior is available through the parent type. System.out.println("Superclass reference: " + reference.summary()); try { new SavingsAccount("Bad", -10, 0.05); } catch (IllegalArgumentException ex) { System.out.println("Rejected invalid account: " + ex.getMessage()); } try { reference.withdraw(10_000); } catch (IllegalArgumentException ex) { System.out.println("Rejected invalid withdrawal: " + ex.getMessage()); } } }