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.
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.
extends ParentClass. The compiler checks that it is allowed to inherit from that class; for example, a final class cannot be extended.super(...). This is why the parent part of the object is built before the child part.Account can call only members declared in Account, even if it points to a SavingsAccount object.is-a: a Circle is a Shape, a Dog is an Animal.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.
| Aspect | Inheritance | Composition |
|---|---|---|
| Relationship | is-a | has-a |
| Coupling | Tighter | Looser |
| Flexibility | Lower | Higher |
| Best for | Shared type family | Reusable parts |
super(...) constructor.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.
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:
is-a, while composition models has-a. If you are only reusing code, composition is often safer because it keeps classes less tightly coupled.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.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.protected to public.is-a, the design will be fragile. In that case, prefer composition or interfaces.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.
super(...).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.
extends connects a subclass to a superclass.super() runs first.is-a relationships.Animal, Dog, and Cat with one overridden method such as speak().has-a example and refactor it from inheritance to composition.