Java Tutorial
🔍

Java Inheritance

Java Inheritance

Inheritance allows one class to acquire the fields and methods of another class. The child class gets everything the parent defined — and can add its own fields, methods, and constructor logic on top. It is the mechanism that turns "write once, reuse everywhere" from a goal into a language feature.

But inheritance is also the most overused OOP concept in Java codebases. Understanding when inheritance is the right choice — and when composition is cleaner — is what separates a developer who knows the syntax from one who designs systems well. Both are tested in interviews; both matter in production.

What Is Inheritance in Java?

Inheritance is an OOP mechanism where a class (the child or subclass) extends another class (the parent or superclass) using the extends keyword. The child class inherits all non-private fields and methods from the parent. It can:

  • Use inherited fields and methods directly
  • Override inherited methods to provide specialised behaviour
  • Add new fields and methods specific to the child
  • Call parent methods and constructors using super

Java supports single inheritance for classes — a class can extend only one other class. Multiple inheritance through classes is not supported in Java. However, a class can implement multiple interfaces, which is how Java achieves multiple-inheritance-like behaviour without the diamond problem.

Types of Inheritance in Java

TypeDescriptionSupported
SingleClass B extends Class AYes
MultilevelClass C extends B, B extends AYes
HierarchicalClasses B and C both extend Class AYes
Multiple (classes)Class C extends both A and BNo — not supported
Multiple (interfaces)Class implements Interface A and BYes
HybridCombination of the abovePartially — through interfaces

Single Inheritance — the extends Keyword

The most fundamental form. One child class extends one parent class.

Java
1// File: Vehicle.java 2 3public class Vehicle { 4 5 private final String registrationNumber; 6 private final String brand; 7 private int speedKmh; 8 9 public Vehicle(String registrationNumber, String brand) { 10 this.registrationNumber = registrationNumber; 11 this.brand = brand; 12 this.speedKmh = 0; 13 } 14 15 public String getRegistrationNumber() { return registrationNumber; } 16 public String getBrand() { return brand; } 17 public int getSpeedKmh() { return speedKmh; } 18 19 public void accelerate(int kmh) { 20 if (kmh > 0) speedKmh += kmh; 21 } 22 23 public void brake(int kmh) { 24 speedKmh = Math.max(0, speedKmh - kmh); 25 } 26 27 public String getStatus() { 28 return brand + " [" + registrationNumber + "] — Speed: " + speedKmh + " km/h"; 29 } 30}
Java
1// File: Car.java 2 3public class Car extends Vehicle { 4 5 private final int numberOfDoors; 6 private boolean airConditionerOn; 7 8 public Car(String registrationNumber, String brand, int numberOfDoors) { 9 super(registrationNumber, brand); // calls Vehicle constructor 10 this.numberOfDoors = numberOfDoors; 11 this.airConditionerOn = false; 12 } 13 14 public int getNumberOfDoors() { return numberOfDoors; } 15 public boolean isAirConditionerOn(){ return airConditionerOn; } 16 17 public void toggleAirConditioner() { 18 airConditionerOn = !airConditionerOn; 19 } 20 21 // Overrides parent's getStatus() to include car-specific details 22 @Override 23 public String getStatus() { 24 return super.getStatus() + " | Doors: " + numberOfDoors 25 + " | AC: " + (airConditionerOn ? "On" : "Off"); 26 } 27}
Java
1// File: SingleInheritanceDemo.java 2 3public class SingleInheritanceDemo { 4 5 public static void main(String[] args) { 6 7 Car car = new Car("MH01AB1234", "Tata Nexon", 4); 8 9 car.accelerate(60); // inherited from Vehicle 10 car.toggleAirConditioner(); // defined in Car 11 12 System.out.println(car.getStatus()); // overridden in Car 13 System.out.println("Brand: " + car.getBrand()); // inherited getter 14 } 15}
Output:
Tata Nexon [MH01AB1234] — Speed: 60 km/h | Doors: 4 | AC: On
Brand: Tata Nexon

Car inherits accelerate(), brake(), getBrand(), and getSpeedKmh() from Vehicle without rewriting them. It adds toggleAirConditioner() for car-specific behaviour and overrides getStatus() to include car details while reusing the parent's status string through super.getStatus().

The super Keyword in Inheritance

super gives a child class access to the parent class's members — its constructor, its fields, and its methods. It is essential when:

  • The child constructor must initialise parent fields
  • The child wants to call the parent version of an overridden method
  • The child needs a parent field that shares a name with a child field

super() — Calling the Parent Constructor

Java
1// File: ElectricCar.java 2 3public class ElectricCar extends Car { 4 5 private final int batteryCapacityKwh; 6 private double chargePercent; 7 8 public ElectricCar(String registrationNumber, String brand, 9 int numberOfDoors, int batteryCapacityKwh) { 10 super(registrationNumber, brand, numberOfDoors); // calls Car constructor 11 this.batteryCapacityKwh = batteryCapacityKwh; 12 this.chargePercent = 100.0; 13 } 14 15 public double getChargePercent() { return chargePercent; } 16 17 public void charge(double percent) { 18 chargePercent = Math.min(100.0, chargePercent + percent); 19 } 20 21 @Override 22 public void accelerate(int kmh) { 23 if (chargePercent <= 5.0) { 24 System.out.println("Battery critically low — cannot accelerate."); 25 return; 26 } 27 super.accelerate(kmh); // reuses Vehicle's accelerate logic 28 chargePercent -= (kmh * 0.1); // drains battery 29 chargePercent = Math.max(0, chargePercent); 30 } 31 32 @Override 33 public String getStatus() { 34 return super.getStatus() // calls Car's getStatus(), which calls Vehicle's 35 + " | Battery: " + String.format("%.1f", chargePercent) + "%" 36 + " | Capacity: " + batteryCapacityKwh + " kWh"; 37 } 38}
Java
1// File: SuperKeywordDemo.java 2 3public class SuperKeywordDemo { 4 5 public static void main(String[] args) { 6 7 ElectricCar ev = new ElectricCar("KA05CD5678", "Tata Nexon EV", 4, 40); 8 9 System.out.println(ev.getStatus()); 10 11 ev.accelerate(80); 12 System.out.println(ev.getStatus()); 13 } 14}
Output:
Tata Nexon EV [KA05CD5678] — Speed: 0 km/h | Doors: 4 | AC: Off | Battery: 100.0% | Capacity: 40 kWh
Tata Nexon EV [KA05CD5678] — Speed: 80 km/h | Doors: 4 | AC: Off | Battery: 92.0% | Capacity: 40 kWh

ElectricCar calls super(registrationNumber, brand, numberOfDoors) to initialise the Car (and through it, the Vehicle) fields. super.accelerate(kmh) reuses the parent's speed logic rather than duplicating it. super.getStatus() chains through Car.getStatus()Vehicle.getStatus(), assembling the full status string layer by layer.

Multilevel Inheritance

A chain where C extends B, and B extends A. The topmost parent's members flow down through the chain.

Java
1// Vehicle → Car → ElectricCar (already shown above) 2// This three-level chain demonstrates multilevel inheritance
Java
1// File: MultilevelDemo.java 2 3public class MultilevelDemo { 4 5 public static void main(String[] args) { 6 7 ElectricCar ev = new ElectricCar("DL01EF9012", "MG ZS EV", 4, 50); 8 9 // Methods from all three levels are accessible on ev 10 ev.accelerate(40); // Vehicle — level 1 11 ev.toggleAirConditioner(); // Car — level 2 12 ev.charge(5.0); // ElectricCar — level 3 13 14 System.out.println(ev.getStatus()); // overridden at each level 15 System.out.println("Reg: " + ev.getRegistrationNumber()); // Vehicle 16 System.out.println("Doors: " + ev.getNumberOfDoors()); // Car 17 System.out.println("Charge: " + ev.getChargePercent() + "%"); // ElectricCar 18 } 19}
Output:
MG ZS EV [DL01EF9012] — Speed: 40 km/h | Doors: 4 | AC: On | Battery: 61.0% | Capacity: 50 kWh
Reg: DL01EF9012
Doors: 4
Charge: 61.0%

ev can access methods from all three levels of the hierarchy. The JVM uses the most specific override — ElectricCar.getStatus() for the full status, Car.toggleAirConditioner() for the AC, and Vehicle.accelerate() (called via super from ElectricCar.accelerate()) for the speed.

Hierarchical Inheritance

Multiple child classes extending the same parent.

Java
1// File: Motorcycle.java 2 3public class Motorcycle extends Vehicle { 4 5 private final String bikeType; // SPORT, CRUISER, COMMUTER 6 7 public Motorcycle(String registrationNumber, String brand, String bikeType) { 8 super(registrationNumber, brand); 9 this.bikeType = bikeType; 10 } 11 12 public String getBikeType() { return bikeType; } 13 14 @Override 15 public String getStatus() { 16 return super.getStatus() + " | Type: " + bikeType; 17 } 18}
Java
1// File: HierarchicalDemo.java 2 3public class HierarchicalDemo { 4 5 public static void main(String[] args) { 6 7 // Both Car and Motorcycle extend Vehicle — hierarchical inheritance 8 Car car = new Car("GJ05GH3456", "Maruti Swift", 4); 9 Motorcycle bike = new Motorcycle("RJ14IJ7890", "Royal Enfield", "CRUISER"); 10 11 car.accelerate(50); 12 bike.accelerate(70); 13 14 System.out.println(car.getStatus()); 15 System.out.println(bike.getStatus()); 16 } 17}
Output:
Maruti Swift [GJ05GH3456] — Speed: 50 km/h | Doors: 4 | AC: Off
Royal Enfield [RJ14IJ7890] — Speed: 70 km/h | Type: CRUISER

Car and Motorcycle are siblings — both inherit from Vehicle independently. Changes to Vehicle propagate to both without touching either child class.

How Inheritance Works Internally

The Object Class

Every class in Java implicitly extends java.lang.Object if no explicit extends is written. Object is the root of every class hierarchy in Java. It provides equals(), hashCode(), toString(), getClass(), and clone() — methods available on every Java object.

java.lang.Object
       |
    Vehicle
    /      \
  Car    Motorcycle
   |
ElectricCar

Constructor Chain — What Happens on new ElectricCar(...)

new ElectricCar(...) is called

  ElectricCar() calls super() → Car()
    Car() calls super() → Vehicle()
      Vehicle() calls super() → Object()
        Object() constructor runs — completes
      Vehicle() body runs — registers fields, speedKmh = 0
    Car() body runs — numberOfDoors, airConditionerOn = false
  ElectricCar() body runs — batteryCapacityKwh, chargePercent = 100.0

Object is fully initialised — returned to caller

Every constructor in the chain must run before the child's body executes. This is why super() must be the first statement in a constructor — it guarantees parent initialisation happens before child fields are set.

Method Resolution — Which Override Runs?

When you call a method on an object, the JVM uses dynamic dispatch — it looks at the actual runtime type of the object, not the reference type, and finds the most specific override.

Java
1Vehicle ref = new ElectricCar("TN01KL1234", "Ola S1 Pro", 0, 3); 2ref.accelerate(30); // calls ElectricCar.accelerate() — not Vehicle.accelerate()

Even though ref is declared as type Vehicle, the method that runs is ElectricCar.accelerate() because that is the actual type of the object. This is runtime polymorphism — and it requires inheritance to work.

Method Overriding Rules

Overriding allows a child class to provide a specific implementation of a method inherited from the parent.

RuleDescription
Same method signatureName, parameter types, and count must match exactly
Same or wider return typeReturn type must be the same or a subtype (covariant return)
Same or less restrictive accessCannot reduce visibility (publicprotected is illegal)
No new checked exceptionsCannot throw checked exceptions not declared in the parent
@Override annotationNot required but strongly recommended — compiler catches mistakes
Cannot override finalMethods declared final in the parent cannot be overridden
Cannot override staticStatic methods are hidden, not overridden — not polymorphic
Cannot override privatePrivate methods are not inherited — not visible to child

Inheritance vs Composition

This is the most important design decision related to inheritance — and the most consistently tested in product-based company interviews.

AspectInheritanceComposition
Relationship"is-a" — Car IS A Vehicle"has-a" — Car HAS AN Engine
CouplingTight — child depends on parent's internalsLoose — component is replaceable
Reuse mechanismAutomatic — inherits all non-private membersExplicit — delegates to the component
FlexibilityLow — hierarchy fixed at compile timeHigh — behaviour swappable at runtime
EncapsulationWeaker — child can access protected parent membersStronger — component is fully encapsulated
OverridingBuilt-inRequires delegation or interface
When to useGenuine "is-a" relationship, stable hierarchyCode reuse without a type relationship
TestingHarder — child tests depend on parent behaviourEasier — component can be mocked
Java limitationSingle class inheritance onlyNo limit — can compose many types

Rule of thumb: If you cannot honestly say "a Child IS A Parent" in the domain, use composition. A Car is genuinely a Vehicle. A Logger is not a UserService — even if UserService needs logging. Give UserService a Logger field; do not extend it.

Composition Example — the Right Alternative

Java
1// File: PaymentLogger.java 2 3public class PaymentLogger { 4 5 public void log(String message) { 6 System.out.println("[PAYMENT LOG] " + message); 7 } 8 9 public void logError(String error) { 10 System.out.println("[PAYMENT ERROR] " + error); 11 } 12}
Java
1// File: PaymentService.java 2 3public class PaymentService { 4 5 private final String serviceId; 6 // Composition — PaymentService HAS A PaymentLogger, not IS A PaymentLogger 7 private final PaymentLogger logger; 8 9 public PaymentService(String serviceId, PaymentLogger logger) { 10 this.serviceId = serviceId; 11 this.logger = logger; 12 } 13 14 public boolean processPayment(String orderId, double amount) { 15 logger.log("Processing Rs." + amount + " for " + orderId); 16 17 if (amount <= 0) { 18 logger.logError("Invalid amount: " + amount + " for " + orderId); 19 return false; 20 } 21 22 // ... actual payment processing logic ... 23 logger.log("Payment confirmed for " + orderId); 24 return true; 25 } 26}
Java
1// File: CompositionDemo.java 2 3public class CompositionDemo { 4 5 public static void main(String[] args) { 6 7 PaymentLogger logger = new PaymentLogger(); 8 PaymentService service = new PaymentService("PAY-SVC-01", logger); 9 10 service.processPayment("ORD-2001", 1499.0); 11 System.out.println(); 12 service.processPayment("ORD-2002", -50.0); 13 } 14}
Output:
[PAYMENT LOG] Processing Rs.1499.0 for ORD-2001
[PAYMENT LOG] Payment confirmed for ORD-2001

[PAYMENT LOG] Processing Rs.-50.0 for ORD-2002
[PAYMENT ERROR] Invalid amount: -50.0 for ORD-2002

PaymentService uses a PaymentLogger without extending it. The logger can be swapped — in tests, a mock logger can be passed instead of the real one. The relationship is correct: a PaymentService is not a PaymentLogger, so it should not inherit from it.

Real-World Example — Employee Payroll System

The Business Problem

You are building the payroll calculation module for a staffing company — similar to what companies like Infosys or Wipro use for their HR systems. The system has a base Employee type with common fields and a basic salary calculation. FullTimeEmployee and ContractEmployee extend it with different pay structures. Both share common behaviour from Employee but calculate getMonthlyPay() differently.

Implementation

Java
1// File: PayrollConfig.java 2 3public final class PayrollConfig { 4 5 public static final double PF_RATE = 0.12; 6 public static final double DA_RATE = 0.10; 7 public static final double HRA_RATE = 0.15; 8 9 private PayrollConfig() {} 10}
Java
1// File: Employee.java 2 3public abstract class Employee { 4 5 private final String employeeId; 6 private final String fullName; 7 private final String department; 8 9 public Employee(String employeeId, String fullName, String department) { 10 this.employeeId = employeeId; 11 this.fullName = fullName; 12 this.department = department; 13 } 14 15 public String getEmployeeId() { return employeeId; } 16 public String getFullName() { return fullName; } 17 public String getDepartment() { return department; } 18 19 // Abstract — each subclass computes pay differently 20 public abstract double getMonthlyPay(); 21 22 // Common payslip format — uses abstract method polymorphically 23 public String getPaySlip() { 24 return String.format("%-12s | %-22s | %-20s | Monthly Pay: Rs.%.2f", 25 employeeId, fullName, department, getMonthlyPay()); 26 } 27}
Java
1// File: FullTimeEmployee.java 2 3public class FullTimeEmployee extends Employee { 4 5 private final double basicSalary; 6 7 public FullTimeEmployee(String employeeId, String fullName, 8 String department, double basicSalary) { 9 super(employeeId, fullName, department); 10 this.basicSalary = basicSalary; 11 } 12 13 public double getBasicSalary() { return basicSalary; } 14 15 @Override 16 public double getMonthlyPay() { 17 double da = basicSalary * PayrollConfig.DA_RATE; 18 double hra = basicSalary * PayrollConfig.HRA_RATE; 19 double pf = basicSalary * PayrollConfig.PF_RATE; 20 return basicSalary + da + hra - pf; 21 } 22}
Java
1// File: ContractEmployee.java 2 3public class ContractEmployee extends Employee { 4 5 private final double dailyRate; 6 private final int daysWorked; 7 8 public ContractEmployee(String employeeId, String fullName, 9 String department, double dailyRate, int daysWorked) { 10 super(employeeId, fullName, department); 11 this.dailyRate = dailyRate; 12 this.daysWorked = daysWorked; 13 } 14 15 public double getDailyRate() { return dailyRate; } 16 public int getDaysWorked() { return daysWorked; } 17 18 @Override 19 public double getMonthlyPay() { 20 // Contract employees: daily rate × days worked, no PF or allowances 21 return dailyRate * daysWorked; 22 } 23}
Java
1// File: PayrollDemo.java 2 3import java.util.List; 4 5public class PayrollDemo { 6 7 public static void main(String[] args) { 8 9 List<Employee> employees = List.of( 10 new FullTimeEmployee("EMP-001", "Ananya Krishnan", 11 "Backend Engineering", 85000.0), 12 new FullTimeEmployee("EMP-002", "Rohan Desai", 13 "DevOps", 72000.0), 14 new ContractEmployee("CON-001", "Priya Mehta", 15 "QA Automation", 3500.0, 22), 16 new ContractEmployee("CON-002", "Karan Singh", 17 "UI Design", 4000.0, 18) 18 ); 19 20 System.out.println("=== Monthly Payroll Report ===\n"); 21 double totalPayroll = 0; 22 23 for (Employee emp : employees) { 24 System.out.println(emp.getPaySlip()); 25 totalPayroll += emp.getMonthlyPay(); 26 } 27 28 System.out.printf("%n Total payroll this month: Rs.%.2f%n", totalPayroll); 29 } 30}
Output:
=== Monthly Payroll Report ===

EMP-001      | Ananya Krishnan        | Backend Engineering  | Monthly Pay: Rs.96050.00
EMP-002      | Rohan Desai            | DevOps               | Monthly Pay: Rs.81360.00
CON-001      | Priya Mehta            | QA Automation        | Monthly Pay: Rs.77000.00
CON-002      | Karan Singh            | UI Design            | Monthly Pay: Rs.72000.00

 Total payroll this month: Rs.326410.00

The for loop works on Employee references — it does not know whether each one is FullTimeEmployee or ContractEmployee. getMonthlyPay() is called polymorphically — the JVM dispatches to the correct subclass implementation. getPaySlip() is defined once in Employee and reused by both subclasses without any duplication.

Best Practices

Use inheritance only for genuine "is-a" relationships

Before adding extends, ask: "Is a Child genuinely a specific kind of Parent in the domain?" A Car is a Vehicle. A SavingsAccount is an Account. A PaymentService is not a Logger. If the answer is no — if you are extending only for code reuse — use composition instead. Inheritance creates a tight coupling that is difficult to reverse once other code depends on the hierarchy.

Keep inheritance hierarchies shallow

Beyond two or three levels, inheritance hierarchies become difficult to reason about. When D extends C extends B extends A, a change to A must be evaluated against B, C, and D simultaneously. Teams following clean architecture treat hierarchies deeper than three levels as a design smell requiring justification.

Always use @Override when overriding methods

The @Override annotation tells the compiler to verify that the method actually overrides something in the parent. Without it, a typo in the method name — getMonthlyPay() accidentally spelled getMonthlyPat() — silently creates a new method instead of overriding, which is a runtime bug that compiles cleanly. Every override in production Java should have @Override.

Do not override methods to do nothing or to throw exceptions

If a subclass must override a parent method to make it do nothing (return immediately) or throw UnsupportedOperationException, the hierarchy is wrong. The child is not a true subtype — it violates the Liskov Substitution Principle. Redesign using composition or an alternative hierarchy.

Common Mistakes

Mistake 1 — Forgetting super() When the Parent Has No No-Arg Constructor

Java
1public class Account { 2 private final String accountId; 3 4 // Only a parameterised constructor — no default no-arg constructor 5 public Account(String accountId) { 6 this.accountId = accountId; 7 } 8} 9 10public class SavingsAccount extends Account { 11 12 private double interestRate; 13 14 public SavingsAccount(String accountId, double interestRate) { 15 // Missing super(accountId) — compiler inserts super() but Account has none 16 // This causes: "constructor Account in class Account cannot be applied to no arguments" 17 this.interestRate = interestRate; 18 } 19}

When the parent has no no-arg constructor, the child must explicitly call super(args) as the first statement. The compiler cannot auto-insert super() if the parent does not have a no-argument version.

Mistake 2 — Using Inheritance for Code Reuse Without an "is-a" Relationship

Java
1// Wrong design — Stack IS NOT an ArrayList conceptually 2// But java.util.Stack extends Vector (a real Java library mistake) 3public class Stack<T> extends ArrayList<T> { 4 // Now Stack inherits add(), remove(), get() and all List methods 5 // Callers can do stack.add(0, element) — inserting at an arbitrary index 6 // This breaks the Stack contract (LIFO only) 7} 8 9// Correct — composition 10public class Stack<T> { 11 private final ArrayList<T> storage = new ArrayList<>(); 12 13 public void push(T element) { storage.add(element); } 14 public T pop() { return storage.remove(storage.size() - 1); } 15 public T peek() { return storage.get(storage.size() - 1); } 16 public boolean isEmpty() { return storage.isEmpty(); } 17}

java.util.Stack extending Vector is a historical mistake in the Java standard library — it exposes List methods that violate stack semantics. A Stack is not truly an ArrayList. This is why Deque implementations like ArrayDeque are now recommended over Stack for stack behaviour in Java.

Mistake 3 — Reducing Visibility of an Overridden Method

Java
1public class Parent { 2 public void display() { 3 System.out.println("Parent display"); 4 } 5} 6 7public class Child extends Parent { 8 // Compile error — cannot reduce visibility from public to protected 9 @Override 10 protected void display() { // error: weaker access privileges 11 System.out.println("Child display"); 12 } 13}

An overriding method cannot be less visible than the parent method. If the parent declares public, the override must be public. Reducing visibility breaks the contract that any caller who has a Parent reference can call display() — if the override is protected, subclass code in other packages could not.

Mistake 4 — Calling Overridable Methods in a Constructor

Java
1public class Base { 2 public Base() { 3 initialise(); // dispatches to Child.initialise() — but Child fields not set yet 4 } 5 public void initialise() { System.out.println("Base initialise"); } 6} 7 8public class Child extends Base { 9 private String name; 10 11 public Child(String name) { 12 super(); // Base() runs first — calls initialise() before name is set 13 this.name = name; 14 } 15 16 @Override 17 public void initialise() { 18 // name is null here — Base constructor called this before Child's body ran 19 System.out.println("Child initialise: " + name); 20 } 21}

When new Child("Alice") runs, super() calls Base(), which calls initialise(). Java dispatches to Child.initialise() because the runtime type is Child. But name has not been assigned yet — the Child constructor body has not run. The result is Child initialise: null. Never call overridable methods from constructors.

Interview Questions

Q1. What is inheritance in Java and what does the extends keyword do?

Inheritance is the OOP mechanism where a class acquires the fields and methods of another class. The extends keyword establishes this relationship — the child class inherits all non-private members of the parent and can add its own. Java supports single class inheritance only — a class can extend exactly one other class. Every class implicitly extends java.lang.Object if no explicit extends is written, making Object the root of every Java class hierarchy.

Q2. What is the difference between method overriding and method hiding in Java?

Method overriding applies to instance methods — when a child class declares a method with the same signature as a parent instance method, it overrides it. The JVM uses dynamic dispatch at runtime to call the most specific override based on the object's actual type. Method hiding applies to static methods — a child static method with the same signature hides the parent's static method. No dynamic dispatch occurs — the method called is determined by the reference type at compile time, not the actual object type. Overriding is polymorphic; hiding is not.

Q3. What is the difference between inheritance and composition in Java?

Inheritance models an "is-a" relationship — Car extends Vehicle means a Car is a Vehicle. Composition models a "has-a" relationship — a Car has an Engine field. Inheritance gives automatic access to parent behaviour but creates tight coupling — changes to the parent ripple to all children. Composition is looser — the component is replaceable and independently testable. Composition should be preferred when the relationship is about code reuse without a genuine type hierarchy. Inheritance is appropriate when the "is-a" relationship is real and stable.

Q4. What is the purpose of the super keyword in Java inheritance?

super provides access to the parent class from within a child class. super() calls the parent's constructor and must be the first statement in the child constructor — it is required when the parent has no no-arg constructor and recommended always for clarity. super.method() calls the parent's version of an overridden method, allowing the child to extend rather than completely replace the parent's behaviour. super.field accesses a parent field that is shadowed by a child field of the same name.

Q5. What is the Liskov Substitution Principle and how does it relate to inheritance?

The Liskov Substitution Principle states that any code that works with a parent type reference must work correctly with a child type reference substituted in its place — without knowing the difference. A well-designed inheritance hierarchy satisfies this: every Car can be used wherever a Vehicle is expected. When a subclass overrides a method to throw an exception, to do nothing, or to change the contract, it violates LSP. This is a signal that the "is-a" relationship is wrong and composition would be more appropriate.

Q6. Does Java support multiple inheritance? How does it handle the diamond problem?

Java does not support multiple inheritance through classes — a class can extend only one other class. This eliminates the diamond problem for classes: if B and C both extend A and override a method, and D extends both B and C, there is no ambiguity — the Java compiler simply does not allow it. Java achieves multiple-inheritance-like behaviour through interfaces, which a class can implement multiple of. When two interfaces provide default method implementations with the same signature, the implementing class must override the method to resolve the ambiguity explicitly — the compiler enforces this.

FAQs

What is inheritance in Java?

Inheritance is the mechanism where a class acquires the fields and methods of another class using the extends keyword. The child class inherits all non-private members, can override inherited methods to specialise behaviour, and can add its own members. It enables code reuse and is the foundation of polymorphism in Java.

What is the difference between single and multilevel inheritance?

Single inheritance is when one class extends exactly one other class — Car extends Vehicle. Multilevel inheritance is a chain — ElectricCar extends Car, Car extends Vehicle. In multilevel inheritance, members flow through the entire chain — ElectricCar inherits from both Car and Vehicle. Java supports both. Multiple inheritance (one class extending two or more classes directly) is not supported for classes.

Can a constructor be inherited in Java?

No. Constructors are not inherited. A child class must define its own constructors. However, a child constructor can — and in many cases must — call the parent's constructor using super() as the first statement. If no explicit super() call is made, the compiler automatically inserts super() (the no-arg parent constructor). If the parent has no no-arg constructor, the child must explicitly call the appropriate parameterised parent constructor.

What is the difference between overriding and overloading?

Overriding is redefining a method inherited from a parent class in the child class — same name, same parameter list, different implementation. It is resolved at runtime (dynamic dispatch). Overloading is defining multiple methods in the same class with the same name but different parameter lists. It is resolved at compile time based on argument types. Overriding enables polymorphism; overloading is a compile-time convenience.

Why does Java not support multiple inheritance through classes?

Java's designers excluded multiple class inheritance to avoid the diamond problem — the ambiguity that arises when two parent classes both define the same method and a class inherits from both. Rather than resolving this ambiguity with complex rules, Java chose a cleaner model: single class inheritance plus multiple interface implementation. Interfaces can have default methods, and when two interfaces conflict on a default method, the implementing class must explicitly resolve it.

What is the difference between IS-A and HAS-A relationships?

IS-A is the inheritance relationship — Car IS A Vehicle, modelled with extends. It means the child is a specific type of the parent and can be used wherever the parent type is expected. HAS-A is the composition relationship — a Car HAS AN Engine, modelled by making Engine a field of Car. It means the class uses another class's functionality without being a type of it. IS-A should be used only when the relationship is genuinely hierarchical in the domain.

Summary

Inheritance gives a child class automatic access to everything a parent defines — fields, methods, and the type relationship that enables polymorphism. The constructor chain guarantees parent initialisation before child setup. Method overriding with dynamic dispatch allows one method call to behave differently depending on the actual object type — the engine of runtime polymorphism.

The discipline of inheritance is knowing when not to use it. When the "is-a" relationship is not genuine, composition produces looser coupling, easier testing, and more flexible design. The rule is simple: extend when a child truly is a parent; compose when it uses a parent.

For interviews, be ready to explain the types of inheritance Java supports and why multiple class inheritance is excluded, demonstrate the constructor chain with super(), articulate the difference between overriding and hiding with concrete examples, and compare inheritance versus composition with a real design decision. These points cover the depth that service-based recall questions and product-based design discussions both probe.

What to Read Next

TopicLink
How polymorphism uses inheritance for runtime method dispatchJava Polymorphism →
How method overriding rules and @Override work in detailJava Method Overriding →
How abstract classes define partial hierarchies for inheritanceJava Abstract Classes →
How interfaces enable multiple-inheritance-like behaviourJava Interfaces →
How the super keyword accesses parent members and constructorsJava super Keyword →
Java Inheritance | DevStackFlow