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.

// File: Vehicle.java public class Vehicle { private final String registrationNumber; private final String brand; private int speedKmh; public Vehicle(String registrationNumber, String brand) { this.registrationNumber = registrationNumber; this.brand = brand; this.speedKmh = 0; } public String getRegistrationNumber() { return registrationNumber; } public String getBrand() { return brand; } public int getSpeedKmh() { return speedKmh; } public void accelerate(int kmh) { if (kmh > 0) speedKmh += kmh; } public void brake(int kmh) { speedKmh = Math.max(0, speedKmh - kmh); } public String getStatus() { return brand + " [" + registrationNumber + "] — Speed: " + speedKmh + " km/h"; } }
// File: Car.java public class Car extends Vehicle { private final int numberOfDoors; private boolean airConditionerOn; public Car(String registrationNumber, String brand, int numberOfDoors) { super(registrationNumber, brand); // calls Vehicle constructor this.numberOfDoors = numberOfDoors; this.airConditionerOn = false; } public int getNumberOfDoors() { return numberOfDoors; } public boolean isAirConditionerOn(){ return airConditionerOn; } public void toggleAirConditioner() { airConditionerOn = !airConditionerOn; } // Overrides parent's getStatus() to include car-specific details @Override public String getStatus() { return super.getStatus() + " | Doors: " + numberOfDoors + " | AC: " + (airConditionerOn ? "On" : "Off"); } }
// File: SingleInheritanceDemo.java public class SingleInheritanceDemo { public static void main(String[] args) { Car car = new Car("MH01AB1234", "Tata Nexon", 4); car.accelerate(60); // inherited from Vehicle car.toggleAirConditioner(); // defined in Car System.out.println(car.getStatus()); // overridden in Car System.out.println("Brand: " + car.getBrand()); // inherited getter } }
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

// File: ElectricCar.java public class ElectricCar extends Car { private final int batteryCapacityKwh; private double chargePercent; public ElectricCar(String registrationNumber, String brand, int numberOfDoors, int batteryCapacityKwh) { super(registrationNumber, brand, numberOfDoors); // calls Car constructor this.batteryCapacityKwh = batteryCapacityKwh; this.chargePercent = 100.0; } public double getChargePercent() { return chargePercent; } public void charge(double percent) { chargePercent = Math.min(100.0, chargePercent + percent); } @Override public void accelerate(int kmh) { if (chargePercent <= 5.0) { System.out.println("Battery critically low — cannot accelerate."); return; } super.accelerate(kmh); // reuses Vehicle's accelerate logic chargePercent -= (kmh * 0.1); // drains battery chargePercent = Math.max(0, chargePercent); } @Override public String getStatus() { return super.getStatus() // calls Car's getStatus(), which calls Vehicle's + " | Battery: " + String.format("%.1f", chargePercent) + "%" + " | Capacity: " + batteryCapacityKwh + " kWh"; } }
// File: SuperKeywordDemo.java public class SuperKeywordDemo { public static void main(String[] args) { ElectricCar ev = new ElectricCar("KA05CD5678", "Tata Nexon EV", 4, 40); System.out.println(ev.getStatus()); ev.accelerate(80); System.out.println(ev.getStatus()); } }
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.

// Vehicle → Car → ElectricCar (already shown above) // This three-level chain demonstrates multilevel inheritance
// File: MultilevelDemo.java public class MultilevelDemo { public static void main(String[] args) { ElectricCar ev = new ElectricCar("DL01EF9012", "MG ZS EV", 4, 50); // Methods from all three levels are accessible on ev ev.accelerate(40); // Vehicle — level 1 ev.toggleAirConditioner(); // Car — level 2 ev.charge(5.0); // ElectricCar — level 3 System.out.println(ev.getStatus()); // overridden at each level System.out.println("Reg: " + ev.getRegistrationNumber()); // Vehicle System.out.println("Doors: " + ev.getNumberOfDoors()); // Car System.out.println("Charge: " + ev.getChargePercent() + "%"); // ElectricCar } }
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.

// File: Motorcycle.java public class Motorcycle extends Vehicle { private final String bikeType; // SPORT, CRUISER, COMMUTER public Motorcycle(String registrationNumber, String brand, String bikeType) { super(registrationNumber, brand); this.bikeType = bikeType; } public String getBikeType() { return bikeType; } @Override public String getStatus() { return super.getStatus() + " | Type: " + bikeType; } }
// File: HierarchicalDemo.java public class HierarchicalDemo { public static void main(String[] args) { // Both Car and Motorcycle extend Vehicle — hierarchical inheritance Car car = new Car("GJ05GH3456", "Maruti Swift", 4); Motorcycle bike = new Motorcycle("RJ14IJ7890", "Royal Enfield", "CRUISER"); car.accelerate(50); bike.accelerate(70); System.out.println(car.getStatus()); System.out.println(bike.getStatus()); } }
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.

Vehicle ref = new ElectricCar("TN01KL1234", "Ola S1 Pro", 0, 3); ref.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 (public → protected 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

// File: PaymentLogger.java public class PaymentLogger { public void log(String message) { System.out.println("[PAYMENT LOG] " + message); } public void logError(String error) { System.out.println("[PAYMENT ERROR] " + error); } }
// File: PaymentService.java public class PaymentService { private final String serviceId; // Composition — PaymentService HAS A PaymentLogger, not IS A PaymentLogger private final PaymentLogger logger; public PaymentService(String serviceId, PaymentLogger logger) { this.serviceId = serviceId; this.logger = logger; } public boolean processPayment(String orderId, double amount) { logger.log("Processing Rs." + amount + " for " + orderId); if (amount <= 0) { logger.logError("Invalid amount: " + amount + " for " + orderId); return false; } // ... actual payment processing logic ... logger.log("Payment confirmed for " + orderId); return true; } }
// File: CompositionDemo.java public class CompositionDemo { public static void main(String[] args) { PaymentLogger logger = new PaymentLogger(); PaymentService service = new PaymentService("PAY-SVC-01", logger); service.processPayment("ORD-2001", 1499.0); System.out.println(); service.processPayment("ORD-2002", -50.0); } }
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.

Basic Example

public class Main { static class Animal { String name; Animal(String name) { this.name = name; } void eat() { System.out.println(name + " is eating."); } } static class Dog extends Animal { Dog(String name) { super(name); } void bark() { System.out.println(name + " says Woof!"); } } public static void main(String[] args) { Dog dog = new Dog("Rex"); dog.eat(); // inherited from Animal dog.bark(); // defined in Dog } }

Output

Rex is eating.
Rex says Woof!

Dog extends Animal, so it automatically gets the eat() method without rewriting it, and adds its own bark() method on top. This is the same single inheritance mechanism shown with Vehicle and Car earlier — just with a smaller, everyday pair of classes.

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

public class Account { private final String accountId; // Only a parameterised constructor — no default no-arg constructor public Account(String accountId) { this.accountId = accountId; } } public class SavingsAccount extends Account { private double interestRate; public SavingsAccount(String accountId, double interestRate) { // Missing super(accountId) — compiler inserts super() but Account has none // This causes: "constructor Account in class Account cannot be applied to no arguments" this.interestRate = interestRate; } }

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

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

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

public class Parent { public void display() { System.out.println("Parent display"); } } public class Child extends Parent { // Compile error — cannot reduce visibility from public to protected @Override protected void display() { // error: weaker access privileges System.out.println("Child display"); } }

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

public class Base { public Base() { initialise(); // dispatches to Child.initialise() — but Child fields not set yet } public void initialise() { System.out.println("Base initialise"); } } public class Child extends Base { private String name; public Child(String name) { super(); // Base() runs first — calls initialise() before name is set this.name = name; } @Override public void initialise() { // name is null here — Base constructor called this before Child's body ran System.out.println("Child initialise: " + name); } }

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

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →