Java Inheritance β extends, super, Overriding & Best Practices
Everything you need to know about Java Inheritance β types of inheritance, extends keyword, super keyword, method overriding, constructor chaining, final keyword, IS-A vs HAS-A, instanceof, Liskov Substitution Principle, anti-patterns, and real-world production code examples.
Last Updated
March 2026
Read Time
27 min
Level
Beginner to Intermediate
Chapter
19 of 35
What is Inheritance in Java?
Inheritance is one of the four pillars of object-oriented programming (alongside Encapsulation, Polymorphism, and Abstraction). It is the mechanism by which one class β called the subclass or child class β acquires the fields and methods of another class β called the superclass or parent class. In Java, inheritance is declared using the extends keyword.
Inheritance models the IS-A relationship between classes. A Dog IS-A Animal. A SavingsAccount IS-A BankAccount. A ElectricCar IS-A Vehicle. When this relationship holds, the subclass can reuse the parent's code, extend it with new behaviour, and substitute for the parent wherever the parent type is expected β this is the essence of runtime polymorphism.
The two core benefits of inheritance are: (1) Code Reuse β common logic written once in the parent is inherited by all subclasses without duplication. (2) Polymorphism β a parent type reference can hold any subclass object, and calling a method on it invokes the subclass's version at runtime. This enables writing flexible, extensible systems where new subclasses can be added without modifying existing code.
The extends Keyword β Establishing Inheritance
The extends keyword is Java's mechanism for declaring that one class inherits from another. It creates a permanent parent-child relationship between the two classes. The subclass automatically receives all non-private members of the superclass and can also define its own additional fields and methods.
public class SubClass extends SuperClass { // Inherited: all public and protected members of SuperClass // Can add: new fields, new methods // Can override: non-private, non-final methods // Cannot override: private methods, static methods (hidden, not overridden) }
β Inherited: public fields, protected fields, public methods, protected methods, default (package-private) members (same package only). β NOT inherited: private fields and methods (hidden within declaring class), constructors (but accessible via super()), static members (accessible but not 'inherited' in polymorphic sense).
A Java class can extend ONLY ONE class β this is single inheritance. 'class C extends A, B' is a compile error. This avoids the Diamond Problem. However, a class CAN implement multiple interfaces: 'class C extends A implements B, D'. And an interface CAN extend multiple interfaces: 'interface C extends A, B'.
// ββ PARENT CLASS ββββββββββββββββββββββββββββββββββββββ
public class Vehicle {
// protected β accessible in subclasses across packages
protected String brand;
protected String model;
protected int year;
protected double fuelLevel; // 0.0 to 1.0
// private β NOT inherited by subclasses
private String vehicleIdentificationNumber;
public Vehicle(String brand, String model, int year) {
this.brand = brand;
this.model = model;
this.year = year;
this.fuelLevel = 1.0;
this.vehicleIdentificationNumber = generateVIN();
}
// public β inherited and can be overridden
public void startEngine() {
System.out.println(brand + " " + model + " engine started.");
}
public void refuel(double amount) {
fuelLevel = Math.min(1.0, fuelLevel + amount);
System.out.printf("Refuelled. Fuel level: %.0f%%%n", fuelLevel * 100);
}
public String getDetails() {
return String.format("%s %s (%d) | Fuel: %.0f%%",
brand, model, year, fuelLevel * 100);
}
// private β NOT accessible in subclasses
private String generateVIN() {
return "VIN-" + System.currentTimeMillis();
}
}
// ββ SUBCLASS β inherits Vehicle βββββββββββββββββββββββ
public class Car extends Vehicle {
private int numberOfDoors;
private String transmissionType; // "Manual" or "Automatic"
public Car(String brand, String model, int year,
int doors, String transmission) {
super(brand, model, year); // β
Must call parent constructor
this.numberOfDoors = doors;
this.transmissionType = transmission;
}
// NEW method β only in Car
public void openTrunk() {
System.out.println(model + " trunk opened.");
}
// OVERRIDING parent method β custom behaviour for Car
@Override
public String getDetails() {
return super.getDetails() + // Reuse parent's version
String.format(" | Doors: %d | %s",
numberOfDoors, transmissionType);
}
}
// ββ ANOTHER SUBCLASS ββββββββββββββββββββββββββββββββββ
public class Truck extends Vehicle {
private double payloadCapacityTons;
public Truck(String brand, String model, int year, double payload) {
super(brand, model, year);
this.payloadCapacityTons = payload;
}
@Override
public void startEngine() {
System.out.println(brand + " " + model +
" heavy engine roaring to life!");
}
public double getPayload() { return payloadCapacityTons; }
}
// ββ USAGE βββββββββββββββββββββββββββββββββββββββββββββ
// Car car = new Car("Toyota", "Camry", 2024, 4, "Automatic");
// Truck truck = new Truck("Tata", "Prima", 2023, 25.0);
// car.startEngine(); // Toyota Camry engine started.
// truck.startEngine(); // Tata Prima heavy engine roaring to life!
// car.openTrunk(); // Camry trunk opened.
// System.out.println(car.getDetails());
// β Toyota Camry (2024) | Fuel: 100% | Doors: 4 | AutomaticTypes of Inheritance in Java
Java supports several forms of inheritance, each modelling a different structural relationship between classes. Understanding these types helps you design class hierarchies that are clean, logical, and extensible.
One class extends exactly ONE parent class. The most common and straightforward form. Example: 'class Car extends Vehicle'. Car inherits all accessible members of Vehicle. Java mandates single inheritance to avoid the Diamond Problem. This is the default and most-used inheritance type in Java applications.
A class extends another class which itself extends another class β forming a chain. Example: Animal β Mammal β Dog. Dog inherits from both Mammal and Animal. The chain can be as long as needed, but deep chains (4+ levels) are generally a code smell β they make the hierarchy fragile and hard to understand. Rule: keep hierarchies shallow.
Multiple classes extend the SAME parent class. Example: Vehicle is extended by Car, Truck, and Motorcycle. All three inherit the common Vehicle behaviour but add their own specialisations. This is the most natural use of inheritance β capturing shared structure in a parent and specialising in children.
Java does NOT allow a class to extend multiple classes. However, a class CAN implement multiple interfaces: 'class Robot implements Movable, Chargeable, Programmable'. And an interface CAN extend multiple interfaces. Java 8+ default methods in interfaces can provide shared implementations, giving most benefits of multiple inheritance without the Diamond Problem.
A combination of multiple inheritance types. Java does not support hybrid inheritance through classes for the same reason it avoids multiple class inheritance. However, hybrid-like structures are achievable using interfaces and abstract classes together β a common real-world pattern in enterprise frameworks like Spring.
// ββ SINGLE INHERITANCE ββββββββββββββββββββββββββββββββ
class Animal {
String name;
void breathe() { System.out.println(name + " breathes."); }
}
class Dog extends Animal { // Single β Dog extends Animal
void bark() { System.out.println(name + " barks!"); }
}
// ββ MULTILEVEL INHERITANCE ββββββββββββββββββββββββββββ
class LivingBeing {
void eat() { System.out.println("Eating..."); }
}
class Animal2 extends LivingBeing { // Level 1
void breathe() { System.out.println("Breathing..."); }
}
class Dog2 extends Animal2 { // Level 2 β inherits from both
void bark() { System.out.println("Barking!"); }
}
// Dog2 object can call: eat(), breathe(), bark()
// ββ HIERARCHICAL INHERITANCE ββββββββββββββββββββββββββ
class Shape {
String colour;
double getArea() { return 0; }
}
class Circle extends Shape { // Child 1
double radius;
@Override double getArea() { return Math.PI * radius * radius; }
}
class Rectangle extends Shape { // Child 2
double width, height;
@Override double getArea() { return width * height; }
}
class Triangle extends Shape { // Child 3
double base, height;
@Override double getArea() { return 0.5 * base * height; }
}
// ββ MULTIPLE INHERITANCE via INTERFACES βββββββββββββββ
interface Flyable { void fly(); }
interface Swimmable { void swim(); }
class Duck extends Animal implements Flyable, Swimmable {
@Override public void fly() { System.out.println(name + " flies."); }
@Override public void swim() { System.out.println(name + " swims."); }
}
// Duck IS-A Animal, IS-A Flyable, IS-A Swimmable β all valid!
// β NOT ALLOWED β multiple class inheritance
// class Mule extends Horse, Donkey { } // Compile errorThe super Keyword β Accessing the Parent
The super keyword in Java is a reference to the immediate parent class. It is the complement to this β where this refers to the current object, super refers to the parent portion of the current object. It has three distinct uses, each critical to working correctly with inheritance.
When a subclass declares a field with the same name as a parent field, the parent's field is 'hidden'. 'super.fieldName' accesses the parent's version. This is rarely needed in well-designed code β field hiding is generally a bad practice. Prefer unique field names in subclasses. But understanding this is essential for exam questions.
When a subclass overrides a parent method, 'super.method()' calls the parent's original version. This is very common β a subclass often wants to EXTEND rather than REPLACE the parent's behaviour. Example: getDetails() in Car calls 'super.getDetails()' to include the parent's output, then appends its own. Without super, the parent logic must be duplicated.
super(args) calls the parent's constructor. MUST be the FIRST statement in the subclass constructor. If not written explicitly, Java auto-inserts 'super()' (no-arg parent constructor). If the parent has no no-arg constructor, omitting super(args) is a compile error. Used to initialise the inherited portion of the object β the parent's fields need to be set up before the subclass extends them.
public class Employee {
protected String name;
protected String department;
protected double baseSalary;
public Employee(String name, String department, double baseSalary) {
this.name = name;
this.department = department;
this.baseSalary = baseSalary;
}
public double calculateMonthlyPay() {
return baseSalary;
}
public String getPaySlip() {
return String.format("Employee: %s | Dept: %s | Base: βΉ%.2f",
name, department, baseSalary);
}
}
// ββ Manager EXTENDS Employee ββββββββββββββββββββββββββ
public class Manager extends Employee {
private double teamBonus;
private int teamSize;
public Manager(String name, String department,
double baseSalary, double teamBonus, int teamSize) {
super(name, department, baseSalary); // β
Use 1: calls Employee constructor
this.teamBonus = teamBonus;
this.teamSize = teamSize;
}
// β
Use 2: call overridden method to extend (not replace) logic
@Override
public double calculateMonthlyPay() {
double base = super.calculateMonthlyPay(); // Parent's version
double bonusShare = teamBonus / teamSize; // Manager's extra
return base + bonusShare;
}
@Override
public String getPaySlip() {
return super.getPaySlip() + // β
Reuse parent's formatted string
String.format(" | Bonus Share: βΉ%.2f | Team: %d",
teamBonus / teamSize, teamSize);
}
}
// ββ SeniorManager EXTENDS Manager (Multilevel) ββββββββ
public class SeniorManager extends Manager {
private double performanceBonus;
public SeniorManager(String name, String department,
double baseSalary, double teamBonus,
int teamSize, double perfBonus) {
super(name, department, baseSalary, teamBonus, teamSize);
this.performanceBonus = perfBonus;
}
@Override
public double calculateMonthlyPay() {
return super.calculateMonthlyPay() + performanceBonus;
}
@Override
public String getPaySlip() {
return super.getPaySlip() +
String.format(" | Perf Bonus: βΉ%.2f", performanceBonus);
}
}
// Usage:
// Employee e = new Employee("Ravi", "Dev", 60000);
// Manager m = new Manager("Priya", "Dev", 90000, 50000, 5);
// SeniorManager s = new SeniorManager("Arjun", "Dev", 130000, 80000, 8, 20000);
// System.out.println(e.getPaySlip());
// System.out.println(m.getPaySlip());
// System.out.println(s.getPaySlip());Constructor Chaining in Inheritance
When an object is created from a subclass, Java does not just call the subclass constructor. It initiates a constructor chain β walking up the entire inheritance hierarchy and executing each class's constructor from the top (Object) downward. This ensures every level of the hierarchy has a chance to initialise its own fields before the subclass adds to them.
When 'new GrandChild()' is called, execution order is: 1. GrandChild() starts, immediately calls super() 2. Child() starts, immediately calls super() 3. Object() runs (java.lang.Object constructor) 4. Child() body continues 5. GrandChild() body continues Each constructor's body runs from TOP (most ancestral) to BOTTOM (most derived). Fields are always initialised before the constructor body they belong to runs.
If a subclass constructor does NOT explicitly call super() or this() as its first statement, Java automatically inserts 'super()' (no-arg parent constructor) at the very beginning. Consequence: if the parent class has NO no-arg constructor (because a parameterised one was defined), the compile fails with 'no suitable constructor found'. Always explicitly call super(args) when the parent has no default constructor.
Both 'this(args)' (same-class constructor chaining) and 'super(args)' (parent constructor) must be the FIRST statement in a constructor. Therefore, a single constructor CANNOT have both. The way it works: 'this(args)' delegates to another constructor in the same class, which eventually calls super(). The super() call bubbles up through 'this()' chains, not directly.
class A {
A() {
System.out.println("A constructor");
}
}
class B extends A {
B() {
// Java auto-inserts: super(); here if not written
System.out.println("B constructor");
}
}
class C extends B {
C() {
// Java auto-inserts: super(); here if not written
System.out.println("C constructor");
}
}
// new C() output:
// A constructor β runs first (top of chain)
// B constructor
// C constructor β runs last (most derived)
// ββ PRACTICAL EXAMPLE βββββββββββββββββββββββββββββββββ
class Account {
protected String accountId;
protected String ownerName;
protected double balance;
Account(String accountId, String ownerName, double initialDeposit) {
if (initialDeposit < 0)
throw new IllegalArgumentException("Initial deposit cannot be negative");
this.accountId = accountId;
this.ownerName = ownerName;
this.balance = initialDeposit;
System.out.println("Account created: " + accountId);
}
}
class SavingsAccount extends Account {
private double interestRate;
private double minimumBalance;
SavingsAccount(String id, String owner,
double deposit, double interestRate) {
super(id, owner, deposit); // β
Initialise Account part first
this.interestRate = interestRate;
this.minimumBalance = 1000.0;
System.out.println("SavingsAccount configured. Rate: " + interestRate);
}
double calculateYearlyInterest() {
return balance * interestRate / 100;
}
}
class FixedDepositAccount extends SavingsAccount {
private int tenureMonths;
private double lockinPenaltyPercent;
FixedDepositAccount(String id, String owner, double deposit,
double rate, int tenure) {
super(id, owner, deposit, rate); // β
Initialise SavingsAccount part
this.tenureMonths = tenure;
this.lockinPenaltyPercent = 1.5;
System.out.println("FD Account. Tenure: " + tenure + " months");
}
}
// new FixedDepositAccount("FD001","Ravi",100000,7.5,24) prints:
// Account created: FD001
// SavingsAccount configured. Rate: 7.5
// FD Account. Tenure: 24 monthsMethod Overriding β Runtime Polymorphism
Method overriding occurs when a subclass provides its own implementation for a method that is already defined in its parent class. It is the mechanism behind runtime polymorphism (dynamic dispatch) β when you call a method on a parent-type reference that actually holds a subclass object, Java calls the subclass's version at runtime, not the parent's.
1. Same method name as parent. 2. Same parameter list (number, types, order). 3. Return type: same, or a covariant (subtype) return. 4. Access modifier: same or WIDER (protectedβpublic β , publicβprotected β). 5. Cannot override: private methods (not visible), static methods (hiding, not overriding), final methods. 6. @Override annotation: not mandatory but strongly recommended β compiler catches mistakes.
When 'Animal a = new Dog(); a.makeSound();' is written, Java doesn't decide which makeSound() to call at compile time β it decides at RUNTIME by looking at the actual object type (Dog), not the reference type (Animal). This is dynamic dispatch. The JVM maintains a virtual method table (vtable) for each class β method calls are resolved through this table at runtime. This is how 'write once, work with many types' polymorphism works.
@Override tells the compiler: 'I intend this to be an override'. The compiler then verifies it actually overrides a parent method. Benefits: (1) Compile error if method name is misspelled. (2) Compile error if parameter list doesn't match. (3) Documents intent clearly. Without @Override, a typo in the method name silently creates a NEW method instead of overriding β a hard-to-find bug.
// ββ PARENT ββββββββββββββββββββββββββββββββββββββββββββ
public abstract class PaymentProcessor {
protected String processorName;
protected double transactionFeePercent;
public PaymentProcessor(String name, double fee) {
this.processorName = name;
this.transactionFeePercent = fee;
}
// Template method β calls overridable processTransaction()
public final PaymentResult pay(double amount, String currency) {
if (amount <= 0)
return PaymentResult.failure("Amount must be positive");
double fee = amount * transactionFeePercent / 100;
double netAmt = amount - fee;
System.out.printf("[%s] Processing %.2f %s (fee: %.2f)%n",
processorName, amount, currency, fee);
return processTransaction(netAmt, currency); // Calls overridden version
}
// β Subclasses MUST override this to provide gateway-specific logic
protected abstract PaymentResult processTransaction(double amount, String currency);
// Can be overridden if needed
public String getProcessorInfo() {
return processorName + " (Fee: " + transactionFeePercent + "%)";
}
}
// ββ SUBCLASS 1: UPI βββββββββββββββββββββββββββββββββββ
public class UPIProcessor extends PaymentProcessor {
private String upiId;
public UPIProcessor(String upiId) {
super("UPI", 0.0); // UPI has zero transaction fee
this.upiId = upiId;
}
@Override
protected PaymentResult processTransaction(double amount, String currency) {
System.out.println(" β UPI transfer to " + upiId);
return PaymentResult.success("UPI-" + System.currentTimeMillis());
}
@Override
public String getProcessorInfo() {
return super.getProcessorInfo() + " | UPI ID: " + upiId;
}
}
// ββ SUBCLASS 2: Credit Card βββββββββββββββββββββββββββ
public class CreditCardProcessor extends PaymentProcessor {
private String maskedCardNumber;
public CreditCardProcessor(String maskedCard) {
super("CreditCard", 1.8); // 1.8% fee
this.maskedCardNumber = maskedCard;
}
@Override
protected PaymentResult processTransaction(double amount, String currency) {
System.out.println(" β Credit card charge on " + maskedCardNumber);
return PaymentResult.success("CC-" + System.currentTimeMillis());
}
}
// ββ RUNTIME POLYMORPHISM IN ACTION ββββββββββββββββββββ
// PaymentProcessor processor; // Parent type reference
//
// processor = new UPIProcessor("ravi@upi");
// processor.pay(1000, "INR"); // Calls UPIProcessor.processTransaction()
//
// processor = new CreditCardProcessor("**** **** 4521");
// processor.pay(5000, "INR"); // Calls CreditCardProcessor.processTransaction()
//
// SAME method call 'processor.pay()' β DIFFERENT behaviour at runtime!final with Inheritance β Preventing Extension & Override
The final keyword interacts with inheritance in three important ways β preventing subclassing, preventing method override, and making fields immutable. These restrictions are deliberate design decisions that protect invariants and communicate intent.
// ββ FINAL CLASS β cannot be extended βββββββββββββββββ
public final class SSLCertificate {
private final String thumbprint;
private final String issuer;
public SSLCertificate(String thumbprint, String issuer) {
this.thumbprint = thumbprint;
this.issuer = issuer;
}
// No subclass can tamper with certificate validation logic
}
// class FakeSSL extends SSLCertificate { } // β Compile error
// ββ FINAL METHOD β cannot be overridden ββββββββββββββ
public class OrderProcessor {
// This core algorithm must never be changed by subclasses
public final OrderResult process(Order order) {
validate(order); // Overridable hook
applyDiscounts(order); // Overridable hook
PaymentResult p = chargePayment(order); // Overridable hook
if (!p.isSuccess()) return OrderResult.failure(p.getReason());
fulfil(order); // Overridable hook
sendConfirmation(order); // Overridable hook
return OrderResult.success(order);
}
// These CAN be overridden β customise the steps
protected void validate(Order order) { /* default impl */ }
protected void applyDiscounts(Order order) { /* default impl */ }
protected PaymentResult chargePayment(Order order){ return PaymentResult.success(""); }
protected void fulfil(Order order) { /* default impl */ }
protected void sendConfirmation(Order order) { /* default impl */ }
}
// Subclass can customise steps but NOT the overall algorithm
public class ExpressOrderProcessor extends OrderProcessor {
@Override
protected void fulfil(Order order) {
System.out.println("Priority fulfillment for order: " + order.getId());
// Express-specific fulfilment logic
}
// @Override
// public OrderResult process(Order o) { } // β Compile error β process() is final
}
// ββ FINAL FIELD β immutable after construction ββββββββ
public class TransactionRecord {
private final String transactionId; // Set once β never changes
private final long timestamp;
private TransactionStatus status; // Mutable β status can change
public TransactionRecord(String id) {
this.transactionId = id;
this.timestamp = System.currentTimeMillis();
this.status = TransactionStatus.PENDING;
}
public String getTransactionId() { return transactionId; }
}IS-A vs HAS-A β Choosing Inheritance or Composition
One of the most important design decisions in OOP is choosing between inheritance (IS-A) and composition (HAS-A). Misusing inheritance when composition is appropriate β or vice versa β leads to fragile, unmaintainable class hierarchies. The litmus test is always: does the relationship truly hold in every possible context?
Use inheritance when: (1) The subclass genuinely IS a more specific type of the parent β a Dog IS-A Animal, a SavingsAccount IS-A BankAccount, an EmailNotification IS-A Notification. (2) The subclass can substitute for the parent in all situations (Liskov Substitution Principle). (3) You want to extend or specialise the parent's behaviour. Red flag: if you need to override a method and throw an exception to say 'not applicable', inheritance is wrong.
Use composition when: (1) One class CONTAINS or USES another β a Car HAS-A Engine, an Order HAS-A PaymentMethod, a Logger HAS-A Formatter. (2) The relationship is 'uses' or 'is made of', not 'is a type of'. (3) You want flexibility β the 'has-a' component can be swapped at runtime. Composition is more flexible than inheritance β you can change behaviours by injecting different implementations.
The classic GoF design principle: 'Favour object composition over class inheritance'. Inheritance creates tight coupling β the subclass is intimately coupled to the parent's implementation. Adding a method to the parent can break subclasses. Composition is looser β the outer class only depends on the component's public API. In Spring and modern Java enterprise code, composition (dependency injection) is strongly preferred over deep inheritance trees.
// ββ IS-A: Correct inheritance βββββββββββββββββββββββββ
public class Notification {
protected String recipientId;
protected String message;
protected NotificationPriority priority;
public Notification(String recipientId, String message,
NotificationPriority priority) {
this.recipientId = recipientId;
this.message = message;
this.priority = priority;
}
public void send() {
System.out.println("Sending to " + recipientId + ": " + message);
}
}
// β
EmailNotification IS-A Notification β inheritance makes sense
public class EmailNotification extends Notification {
private String subject;
private String fromAddress;
public EmailNotification(String recipientId, String subject,
String body, String from) {
super(recipientId, body, NotificationPriority.NORMAL);
this.subject = subject;
this.fromAddress = from;
}
@Override
public void send() {
System.out.printf("Email β %s | Subject: %s | From: %s%n",
recipientId, subject, fromAddress);
}
}
// ββ HAS-A: Correct composition ββββββββββββββββββββββββ
public class Engine {
private int horsePower;
private String fuelType;
public Engine(int hp, String fuelType) {
this.horsePower = hp;
this.fuelType = fuelType;
}
public void start() { System.out.println(horsePower + "hp engine started."); }
public void stop() { System.out.println("Engine stopped."); }
public int getHP() { return horsePower; }
}
// β
Car HAS-A Engine β composition is correct
// β Car extends Engine β WRONG: a Car is NOT a type of Engine
public class Car {
private final Engine engine; // HAS-A relationship
private String brand;
private boolean running;
public Car(String brand, Engine engine) {
this.brand = brand;
this.engine = engine; // Engine injected β can swap petrol/electric
this.running = false;
}
public void start() {
engine.start(); // DELEGATES to the Engine object
running = true;
System.out.println(brand + " is now running.");
}
// Benefit: engine can be swapped β ElectricEngine, HybridEngine, etc.
public int getEnginePower() { return engine.getHP(); }
}
// Car car = new Car("Honda", new Engine(150, "Petrol"));
// car.start(); // 150hp engine started. β Honda is now running.instanceof Operator β Type Checking in Inheritance
The instanceof operator tests whether an object is an instance of a specific class, or of a class that inherits from it. It returns true if the object IS-A that type (directly or through inheritance), and false otherwise. Java 16+ enhances this with Pattern Matching instanceof β combining the type check with an automatic cast.
'obj instanceof ClassName' β returns true if obj is an instance of ClassName or any of its subclasses. Always returns false for null (never throws NullPointerException). Example: in an inheritance chain Animal β Mammal β Dog, a Dog object is instanceof Dog (true), instanceof Mammal (true), instanceof Animal (true), instanceof Object (true).
'if (shape instanceof Circle c)' β tests AND casts in one step. If true, 'c' is automatically bound as a Circle reference β no explicit cast needed. The binding variable is in scope only where guaranteed to match. Eliminates the verbose test-then-cast boilerplate. Combined with && for additional conditions: 'if (obj instanceof String s && s.length() > 5)'.
A subclass object is instanceof its parent, grandparent, and all ancestors β up to Object. 'new Dog() instanceof Animal' β true. 'new Dog() instanceof Object' β true. This is the foundation of polymorphism β you can treat any object through its parent type interface. Use instanceof when you genuinely need to know the specific type; avoid it when polymorphism (method overriding) can handle it more cleanly.
public class InstanceofDemo {
// ββ Class hierarchy βββββββββββββββββββββββββββββββ
static class Shape { double getArea() { return 0; } }
static class Circle extends Shape {
double radius;
Circle(double r) { radius = r; }
@Override double getArea() { return Math.PI * radius * radius; }
}
static class Rectangle extends Shape {
double w, h;
Rectangle(double w, double h) { this.w=w; this.h=h; }
@Override double getArea() { return w * h; }
}
static class Square extends Rectangle {
Square(double side) { super(side, side); }
}
public static void main(String[] args) {
Shape[] shapes = {
new Circle(5),
new Rectangle(4, 6),
new Square(3),
null
};
for (Shape s : shapes) {
// ββ Classic instanceof ββββββββββββββββββββ
System.out.println("--- Next shape ---");
System.out.println("instanceof Shape: " + (s instanceof Shape));
System.out.println("instanceof Circle: " + (s instanceof Circle));
System.out.println("instanceof Rectangle: " + (s instanceof Rectangle));
System.out.println("instanceof Square: " + (s instanceof Square));
// null instanceof anything β always false (no NPE!)
}
// ββ Pattern Matching instanceof (Java 16+) ββββ
Shape shape = new Circle(7.0);
// Old way β verbose: test + separate cast
if (shape instanceof Circle) {
Circle c = (Circle) shape; // Redundant cast
System.out.println("Old way β Radius: " + c.radius);
}
// β
New way β test + bind in one step (Java 16+)
if (shape instanceof Circle c) {
System.out.println("New way β Radius: " + c.radius);
System.out.println("Area: " + String.format("%.2f", c.getArea()));
}
// β
With extra condition on the bound variable
if (shape instanceof Circle c && c.radius > 5.0) {
System.out.println("Large circle β radius: " + c.radius);
}
// ββ Preferred: Polymorphism over instanceof βββ
// Instead of instanceof + cast, rely on overriding:
for (Shape s2 : new Shape[]{ new Circle(3), new Rectangle(2,4) }) {
System.out.println("Area: " + String.format("%.2f", s2.getArea()));
// No instanceof needed β dynamic dispatch handles it!
}
}
}java.lang.Object β The Root of All Java Classes
Every class in Java implicitly extends java.lang.Object β even if you don't write extends Object. This means every object, no matter what class it belongs to, inherits a set of core methods from Object. Understanding these methods is essential β some require overriding to work correctly, and their contracts have precise rules.
import java.util.Objects;
public class OrderItem {
private final String productId;
private final String productName;
private final int quantity;
private final double unitPrice;
public OrderItem(String productId, String name, int qty, double price) {
this.productId = productId;
this.productName = name;
this.quantity = qty;
this.unitPrice = price;
}
// ββ toString: human-readable representation βββββββ
@Override
public String toString() {
return String.format("OrderItem{id='%s', name='%s', qty=%d, price=βΉ%.2f, total=βΉ%.2f}",
productId, productName, quantity, unitPrice, getTotal());
}
// ββ equals: value-based equality ββββββββββββββββββ
@Override
public boolean equals(Object o) {
if (this == o) return true; // Same object
if (!(o instanceof OrderItem item)) return false; // Different type or null
return quantity == item.quantity
&& Double.compare(unitPrice, item.unitPrice) == 0
&& Objects.equals(productId, item.productId);
}
// ββ hashCode: consistent with equals ββββββββββββββ
@Override
public int hashCode() {
return Objects.hash(productId, quantity, unitPrice);
}
// Business method
public double getTotal() { return quantity * unitPrice; }
// Getters
public String getProductId() { return productId; }
public String getProductName() { return productName; }
public int getQuantity() { return quantity; }
public double getUnitPrice() { return unitPrice; }
}
// Usage demonstration:
// OrderItem a = new OrderItem("P001", "Laptop", 1, 65000);
// OrderItem b = new OrderItem("P001", "Laptop", 1, 65000);
// System.out.println(a); // OrderItem{id='P001', name='Laptop'...}
// System.out.println(a.equals(b)); // true β value equality
// System.out.println(a == b); // false β different objects in memory
//
// var set = new java.util.HashSet<OrderItem>();
// set.add(a);
// set.contains(b); // true β works correctly with hashCode overrideLiskov Substitution Principle β The Test of Good Inheritance
The Liskov Substitution Principle (LSP) β the 'L' in SOLID β states: if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program. In plain English: wherever you use a Parent object, you should be able to substitute a Child object and everything should still work correctly.
A subclass honours the parent's contract: (1) Does not weaken preconditions β accepts at least the same inputs. (2) Does not strengthen postconditions β delivers at least the same guarantees. (3) Does not throw new exceptions not declared by the parent. (4) Preserves invariants β properties that always hold for the parent type. If Dog extends Animal and Animal.makeSound() is supposed to make a non-silent sound, Dog.makeSound() must also make a sound.
The Rectangle-Square problem: Square extends Rectangle seems logical mathematically. But if Rectangle has setWidth() and setHeight() and code sets them independently, Square breaks the contract β setting width also changes height (to stay square), violating the caller's expectation that width and height are independent. The fix: Square should NOT extend Rectangle β they should share a common Shape interface instead.
Warning signs: (1) Overriding a method to throw UnsupportedOperationException ('not applicable'). (2) Overriding a method to do nothing (empty body) when the parent's version does something important. (3) Strengthening input validation in an override (rejecting valid parent inputs). (4) Overriding to return null when the parent guarantees non-null. Any of these means inheritance is the wrong tool β use composition or a shared interface instead.
// ββ LSP VIOLATION: Rectangle-Square Problem βββββββββββ
class Rectangle {
protected double width;
protected double height;
public void setWidth(double w) { this.width = w; }
public void setHeight(double h) { this.height = h; }
public double getArea() { return width * height; }
}
// β VIOLATES LSP β Square breaks Rectangle's behaviour contract
class Square extends Rectangle {
@Override
public void setWidth(double w) {
this.width = w;
this.height = w; // Forces equal sides β breaks Rectangle contract!
}
@Override
public void setHeight(double h) {
this.width = h; // Same problem
this.height = h;
}
}
// Code that works for Rectangle but BREAKS for Square:
static void testRectangle(Rectangle r) {
r.setWidth(5);
r.setHeight(3);
// Expects area = 15 for ANY Rectangle
assert r.getArea() == 15 : "Expected 15 but got " + r.getArea();
}
// testRectangle(new Rectangle()); // β
Passes
// testRectangle(new Square()); // β Fails β area is 9, not 15!
// ββ LSP-COMPLIANT FIX: Use abstraction instead ββββββββ
interface Shape {
double getArea();
double getPerimeter();
}
class RectangleFixed implements Shape {
private final double width;
private final double height;
RectangleFixed(double w, double h) { width=w; height=h; }
@Override public double getArea() { return width * height; }
@Override public double getPerimeter() { return 2*(width+height); }
}
class SquareFixed implements Shape {
private final double side;
SquareFixed(double s) { side = s; }
@Override public double getArea() { return side * side; }
@Override public double getPerimeter() { return 4 * side; }
}
// Both implement Shape β no LSP violation.
// Square is not forced to pretend it's a mutable Rectangle.
// ββ LSP-COMPLIANT Inheritance Example βββββββββββββββββ
abstract class Discount {
// Contract: always returns a value between 0.0 and 1.0
public abstract double getDiscountRate(double orderValue);
}
class FestivalDiscount extends Discount {
@Override
public double getDiscountRate(double orderValue) {
return orderValue > 5000 ? 0.20 : 0.10; // β
Always 0.0β1.0
}
}
class PremiumMemberDiscount extends Discount {
@Override
public double getDiscountRate(double orderValue) {
return 0.15; // β
Always 0.0β1.0 β LSP honoured
}
}Common Mistakes & Pitfalls β Bugs That Trip Everyone Up
These inheritance-related mistakes appear consistently in Java beginner and intermediate code. Each one either produces a compile-time error or β more dangerously β a silent runtime bug that is difficult to trace.
// β MISTAKE 1: Forgetting super() when parent has no no-arg constructor
class Vehicle {
String brand;
Vehicle(String brand) { this.brand = brand; } // No default constructor!
}
class Car extends Vehicle {
int doors;
Car(int doors) {
// Java auto-inserts super() β but Vehicle has no no-arg constructor!
// β Compile error: no suitable constructor found
this.doors = doors;
}
}
// β
Fix: explicitly call super(args)
class CarFixed extends Vehicle {
int doors;
CarFixed(String brand, int doors) {
super(brand); // β
Calls Vehicle(String brand)
this.doors = doors;
}
}
// β MISTAKE 2: @Override typo silently creates a NEW method
class Animal {
public void makeSound() { System.out.println("..."); }
}
class Cat extends Animal {
// Missing @Override β 'makesound' is a NEW method, not an override
public void makesound() { System.out.println("Meow"); } // Silent bug!
}
// Animal a = new Cat();
// a.makeSound(); // Prints "..." β Cat's method never called!
// β
Fix: always add @Override β compiler will catch the typo
// β MISTAKE 3: Narrowing access modifier in override
class Base {
public void display() { System.out.println("Base"); }
}
class Derived extends Base {
// @Override
// protected void display() { } // β Compile error β narrows public to protected
@Override
public void display() { System.out.println("Derived"); } // β
Same or wider
}
// β MISTAKE 4: Calling overridable method from constructor
class Parent {
public Parent() {
printInfo(); // β Dangerous β calls overridden version before subclass is ready
}
public void printInfo() { System.out.println("Parent info"); }
}
class Child extends Parent {
private String info = "Child data";
@Override
public void printInfo() {
System.out.println(info.toUpperCase()); // β NPE! 'info' is null here
}
}
// new Child() β NullPointerException in printInfo() before info is set
// β MISTAKE 5: instanceof misused β checking specific type when polymorphism works
// BAD: every time Animal adds a new subclass, this switch breaks
for (Animal a : animals) {
if (a instanceof Dog) { ((Dog)a).fetch(); }
else if (a instanceof Cat) { ((Cat)a).purr(); }
else if (a instanceof Bird) { ((Bird)a).tweet(); }
}
// β
Better: use polymorphism β each subclass overrides 'performAction()'
for (Animal a : animals) {
a.performAction(); // Dog.performAction() fetches, Cat.purr()s, etc.
}Bad Practices & Anti-Patterns β What Senior Developers Reject
These inheritance anti-patterns are the most common causes of fragile, unmaintainable class hierarchies in professional Java codebases. Each one has a cleaner alternative.
Classes 5+ levels deep β every change to the top of the hierarchy ripples to all descendants. Joe Armstrong (Erlang creator) famously said: 'You wanted a banana but you got a gorilla holding the banana and the entire jungle.' Deep chains are fragile, hard to understand, and signal that composition may be more appropriate. Rule: inheritance hierarchies beyond 3 levels are a code smell.
Using inheritance ONLY because a parent has methods you want β not because a true IS-A relationship exists. Example: 'class Stack extends ArrayList' to reuse ArrayList's storage. But a Stack IS NOT an ArrayList β Stack should restrict access to top only. Result: all ArrayList methods (get(), add(index), remove(index)) are exposed on Stack, breaking encapsulation. Fix: composition β Stack CONTAINS an ArrayList internally.
Overriding a method and throwing UnsupportedOperationException to say 'this method doesn't apply here' is a clear LSP violation. If a subclass cannot honour the parent's method contract, the inheritance is wrong. Real example: java.util.Collections.unmodifiableList() adds() throws UnsupportedOperationException β it shouldn't extend a mutable list at all. Fix: use a separate interface or composition.
Making fields 'protected' instead of 'private' because subclasses need them directly is lazy encapsulation. Protected fields tightly couple every subclass to the parent's internal representation. If the parent changes how it stores data, all subclasses break. Fix: keep fields private in the parent and provide protected methods (getters, callbacks) for subclasses to interact with them safely.
Extending a concrete class from a third-party library you don't control is risky β the library may add final methods, change behaviour, or the class may not be designed for extension. This creates a tight coupling to an external implementation detail. Fix: wrap the class with composition (Decorator/Adapter pattern). Only extend when the class was explicitly designed for extension.
If a subclass adds fields that are part of equality (e.g., a GoldMember extends Member, adds a 'membershipLevel' field), failing to override equals() and hashCode() in the subclass means two GoldMembers with different levels may compare as equal using the parent's equals(). This breaks Set and Map behaviour. JEP note: Java's Effective Java (Bloch) recommends avoiding extending concrete classes when value equality is involved β use composition.
Real-World Production Code Examples β Inheritance in Context
The following examples demonstrate carefully designed inheritance hierarchies from real enterprise Java applications β showing how inheritance, abstract classes, method overriding, and the template method pattern work together in production-grade code.
package com.techsustainify.report;
import java.time.LocalDateTime;
import java.util.List;
/**
* Abstract base class β defines the TEMPLATE for report generation.
* The algorithm skeleton is fixed (final); steps are customisable (overridable).
* Pattern: Template Method β GoF Behavioural Pattern
*/
public abstract class ReportGenerator {
protected final String reportTitle;
protected final String generatedBy;
protected final LocalDateTime generatedAt;
protected ReportGenerator(String title, String generatedBy) {
this.reportTitle = title;
this.generatedBy = generatedBy;
this.generatedAt = LocalDateTime.now();
}
// ββ TEMPLATE METHOD β algorithm skeleton, cannot be overridden ββ
public final ReportOutput generate(ReportRequest request) {
validateRequest(request); // Hook 1
List<?> data = fetchData(request); // Hook 2 β must override
List<?> filtered = filterData(data, request); // Hook 3
List<?> sorted = sortData(filtered); // Hook 4
ReportBody body = formatBody(sorted); // Hook 5 β must override
ReportHeader header = buildHeader(request); // Hook 6
ReportFooter footer = buildFooter(); // Hook 7
return assembleReport(header, body, footer);
}
// ββ ABSTRACT HOOKS β subclasses MUST implement ββββ
protected abstract List<?> fetchData(ReportRequest request);
protected abstract ReportBody formatBody(List<?> data);
// ββ OVERRIDABLE HOOKS β subclasses MAY customise ββ
protected void validateRequest(ReportRequest request) {
if (request == null)
throw new IllegalArgumentException("Request cannot be null");
}
protected List<?> filterData(List<?> data, ReportRequest request) {
return data; // Default: no filtering
}
protected List<?> sortData(List<?> data) {
return data; // Default: preserve order
}
protected ReportHeader buildHeader(ReportRequest request) {
return new ReportHeader(reportTitle, generatedBy, generatedAt);
}
protected ReportFooter buildFooter() {
return new ReportFooter("Page 1 of 1", "Confidential");
}
private ReportOutput assembleReport(ReportHeader h,
ReportBody b, ReportFooter f) {
return new ReportOutput(h, b, f);
}
}
// ββ CONCRETE SUBCLASS 1: Sales Report βββββββββββββββββ
public class SalesReportGenerator extends ReportGenerator {
private final SalesRepository salesRepo;
public SalesReportGenerator(SalesRepository repo, String generatedBy) {
super("Monthly Sales Report", generatedBy);
this.salesRepo = repo;
}
@Override
protected List<SaleRecord> fetchData(ReportRequest request) {
return salesRepo.findByDateRange(
request.getFromDate(), request.getToDate());
}
@Override
protected List<?> sortData(List<?> data) {
// Sales reports sorted by date descending
return ((List<SaleRecord>) data).stream()
.sorted((a, b) -> b.getSaleDate().compareTo(a.getSaleDate()))
.toList();
}
@Override
protected ReportBody formatBody(List<?> data) {
List<SaleRecord> sales = (List<SaleRecord>) data;
double totalRevenue = sales.stream().mapToDouble(SaleRecord::getAmount).sum();
return new SalesReportBody(sales, totalRevenue);
}
}
// ββ CONCRETE SUBCLASS 2: Inventory Report βββββββββββββ
public class InventoryReportGenerator extends ReportGenerator {
private final InventoryRepository inventoryRepo;
private final int lowStockThreshold;
public InventoryReportGenerator(InventoryRepository repo,
int threshold, String generatedBy) {
super("Inventory Status Report", generatedBy);
this.inventoryRepo = repo;
this.lowStockThreshold = threshold;
}
@Override
protected List<InventoryItem> fetchData(ReportRequest request) {
return inventoryRepo.findAll();
}
@Override
protected List<?> filterData(List<?> data, ReportRequest request) {
// Only include low-stock items
return ((List<InventoryItem>) data).stream()
.filter(item -> item.getStock() < lowStockThreshold)
.toList();
}
@Override
protected ReportBody formatBody(List<?> data) {
return new InventoryReportBody((List<InventoryItem>) data, lowStockThreshold);
}
}
// Both generators use the SAME template (generate()) but produce
// completely different reports through method overriding.package com.techsustainify.notification;
import java.time.LocalDateTime;
// ββ ROOT: Abstract Notification βββββββββββββββββββββββ
public abstract class BaseNotification {
private final String id;
private final String recipientId;
private final String body;
private final NotificationPriority priority;
private final LocalDateTime createdAt;
private NotificationStatus status;
protected BaseNotification(String recipientId, String body,
NotificationPriority priority) {
this.id = java.util.UUID.randomUUID().toString();
this.recipientId = recipientId;
this.body = body;
this.priority = priority;
this.createdAt = LocalDateTime.now();
this.status = NotificationStatus.PENDING;
}
// Template method β fixed delivery flow
public final DeliveryResult deliver() {
try {
if (!isRecipientReachable()) {
status = NotificationStatus.FAILED;
return DeliveryResult.failure("Recipient unreachable");
}
DeliveryResult result = doDeliver();
status = result.isSuccess()
? NotificationStatus.DELIVERED
: NotificationStatus.FAILED;
return result;
} catch (Exception e) {
status = NotificationStatus.FAILED;
return DeliveryResult.failure("Delivery error: " + e.getMessage());
}
}
// Each notification type implements its own delivery
protected abstract DeliveryResult doDeliver();
protected abstract boolean isRecipientReachable();
// Getters
public String getId() { return id; }
public String getRecipientId() { return recipientId; }
public String getBody() { return body; }
public NotificationPriority getPriority() { return priority; }
public NotificationStatus getStatus() { return status; }
public boolean isPending() { return status == NotificationStatus.PENDING; }
}
// ββ EMAIL NOTIFICATION ββββββββββββββββββββββββββββββββ
public class EmailNotification extends BaseNotification {
private final String subject;
private final String toEmail;
private final EmailClient emailClient;
public EmailNotification(String recipientId, String toEmail,
String subject, String body,
EmailClient client) {
super(recipientId, body, NotificationPriority.NORMAL);
this.subject = subject;
this.toEmail = toEmail;
this.emailClient = client;
}
@Override
protected boolean isRecipientReachable() {
return toEmail != null && toEmail.contains("@");
}
@Override
protected DeliveryResult doDeliver() {
return emailClient.send(toEmail, subject, getBody());
}
}
// ββ SMS NOTIFICATION ββββββββββββββββββββββββββββββββββ
public class SmsNotification extends BaseNotification {
private final String phoneNumber;
private final SmsGateway smsGateway;
public SmsNotification(String recipientId, String phoneNumber,
String body, SmsGateway gateway) {
super(recipientId, body, NotificationPriority.HIGH);
this.phoneNumber = phoneNumber;
this.smsGateway = gateway;
}
@Override
protected boolean isRecipientReachable() {
return phoneNumber != null && phoneNumber.matches("[6-9]\\d{9}");
}
@Override
protected DeliveryResult doDeliver() {
return smsGateway.sendSms(phoneNumber, getBody());
}
}
// ββ Dispatch service uses parent type β polymorphism ββ
public class NotificationDispatcher {
public void dispatch(List<BaseNotification> notifications) {
notifications.stream()
.filter(BaseNotification::isPending)
.forEach(n -> {
DeliveryResult r = n.deliver(); // Dynamic dispatch
System.out.printf("[%s] %s β %s%n",
n.getClass().getSimpleName(),
n.getRecipientId(),
r.isSuccess() ? "Delivered" : "Failed: " + r.getReason());
});
}
}Inheritance Hierarchy Flowchart β Object Creation Chain
This flowchart illustrates what happens at the JVM level when you create a subclass object β showing the constructor chain, method resolution, and memory layout.
Code Execution Flow β from source to output
Java Inheritance Interview Questions β Beginner to Advanced
These questions are consistently asked in Java fresher and experienced developer interviews, campus placements, and OCPJP certification exams.
Practice Questions β Test Your Inheritance Knowledge
Attempt each question independently before reading the answer β active recall significantly improves retention and understanding.
1. What is the output? class A { A() { System.out.println("A"); } } class B extends A { B() { System.out.println("B"); } } class C extends B { C() { System.out.println("C"); } } public class Test { public static void main(String[] args) { new C(); } }
Easy2. Will this compile? If not, why? class Animal { Animal(String name) { System.out.println("Animal: " + name); } } class Dog extends Animal { Dog() { System.out.println("Dog created"); } }
Easy3. What is the output? class Parent { String name = "Parent"; void display() { System.out.println("Parent: " + name); } } class Child extends Parent { String name = "Child"; // Field hiding @Override void display() { System.out.println("Child: " + name); } } public class Test { public static void main(String[] args) { Parent p = new Child(); p.display(); System.out.println(p.name); } }
Medium4. Design a class hierarchy for a Shape system: Shape (abstract, getArea(), getPerimeter()), Circle, Rectangle, and Square. Square should use composition or correct inheritance. Add a ShapeCalculator that works with any Shape.
Medium5. Identify the LSP violation: class Bird { public void fly() { System.out.println("Flying..."); } public void eat() { System.out.println("Eating..."); } } class Penguin extends Bird { @Override public void fly() { throw new UnsupportedOperationException("Penguins cannot fly!"); } }
Medium6. What is the output? class Base { static void staticMethod() { System.out.println("Base static"); } void instanceMethod() { System.out.println("Base instance"); } } class Derived extends Base { static void staticMethod() { System.out.println("Derived static"); } @Override void instanceMethod() { System.out.println("Derived instance"); } } public class Test { public static void main(String[] args) { Base obj = new Derived(); obj.staticMethod(); obj.instanceMethod(); } }
Hard7. Write a multilevel inheritance example: LivingBeing β Animal β Pet β Dog. Each class adds one field and one method. Show constructor chaining.
Hard8. Explain why 'class Stack extends ArrayList' is a bad design. What is the correct approach?
HardConclusion β Inheritance: Power with Responsibility
Inheritance is one of Java's most powerful features β and one of the most frequently misused. When used correctly, it enables clean code reuse, elegant polymorphism, and extensible architectures. When misused β for code reuse without a true IS-A relationship, or in violation of LSP β it creates fragile, tightly-coupled hierarchies that are painful to maintain.
The professional's approach: always ask the IS-A question first. Does this relationship hold in every context, without exception? Does the subclass honour the parent's contract completely? Can I substitute the child wherever the parent is expected? If yes to all β inheritance is right. If no β composition is likely the better tool. The Template Method, Decorator, Strategy, and Composite patterns often provide better design alternatives than deep inheritance trees.
Your next step: Java Polymorphism β where you will see how inheritance and method overriding combine to create programs that work uniformly across many types, how interfaces extend polymorphism beyond class hierarchies, and how modern Java (sealed classes, records, pattern matching switch) takes these concepts further. β