☕ Java
Multilevel Inheritance
Multilevel inheritance forms a chain of classes where each class extends the one above it — A extends B, B extends C, creating a three-level hierarchy. The bottom class inherits from all classes above it in the chain. Java supports arbitrary depth in inheritance chains; every class ultimately extends Object at the top. Multilevel inheritance enables progressive specialisation — each level adds specificity while inheriting the full behaviour of all levels above it. This entry covers how inheritance chains work, constructor execution order through multiple levels, method resolution with hidden and overridden methods, the limits of deep hierarchies, and how to use super across multiple levels.
Inheritance Chains — How Multilevel Works
In multilevel inheritance each class in the chain is both a subclass (of the class above it) and a superclass (of the class below it). The most derived class — the one at the bottom of the chain — inherits from every class above it transitively. All non-private members are available: the most derived class can call methods defined three levels up, and a reference typed as the topmost class can hold an instance of the most derived class.
The transitive nature of inheritance means that every class in the chain is an instance of every class above it. If Animal → Mammal → Dog, then a Dog is a Mammal, a Dog is an Animal, and a Dog is an Object. The instanceof operator confirms all of these relationships at runtime. This transitivity is what makes polymorphism so powerful in deep hierarchies — code written to work with Animal automatically works with Dog, Cat, Horse, and any other animal subclass added in the future.
Each level in the chain represents a meaningful level of abstraction. Animal captures what is common to all animals. Mammal adds what is specific to mammals but common to all of them. Dog adds what is specific to dogs. When you model a domain with multilevel inheritance, each class should represent a genuine natural category at its level of abstraction — if you find yourself skipping levels or creating classes with no behaviour of their own, the hierarchy is probably too deep.
Java
// ── Three-level inheritance chain ────────────────────────────────────
// Object (implicit root)
// └── Animal
// └── Mammal
// └── Dog
public class Animal {
private final String name;
protected int heartRate;
public Animal(String name, int heartRate) {
this.name = name;
this.heartRate = heartRate;
}
public void breathe() {
System.out.println(name + " breathes.");
}
public void eat(String food) {
System.out.println(name + " eats " + food + ".");
}
public String getName() { return name; }
public int getHeartRate(){ return heartRate; }
@Override
public String toString() {
return "Animal[" + name + "]";
}
}
public class Mammal extends Animal {
private final boolean furCovered;
private double bodyTemperature;
public Mammal(String name, int heartRate, boolean furCovered) {
super(name, heartRate); // call Animal constructor
this.furCovered = furCovered;
this.bodyTemperature = 37.0;
}
public void nurseYoung() {
System.out.println(getName() + " nurses young with milk.");
}
public void regulateTemperature() {
System.out.println(getName() +
" maintains " + bodyTemperature + "°C body temperature.");
}
public boolean isFurCovered() { return furCovered; }
public double getBodyTemperature() { return bodyTemperature; }
@Override
public String toString() {
return "Mammal[" + getName() + " fur=" + furCovered + "]";
}
}
public class Dog extends Mammal {
private final String breed;
private boolean trained;
public Dog(String name, String breed) {
super(name, 80, true); // call Mammal constructor
this.breed = breed;
this.trained = false;
}
public void bark() { System.out.println(getName() + " barks!"); }
public void fetch() { System.out.println(getName() + " fetches!"); }
public String getBreed() { return breed; }
public boolean isTrained() { return trained; }
@Override
public String toString() {
return "Dog[" + getName() + " breed=" + breed + "]";
}
}
// ── Transitive is-a relationships ─────────────────────────────────────
Dog rex = new Dog("Rex", "Labrador");
System.out.println(rex instanceof Dog); // true
System.out.println(rex instanceof Mammal); // true — transitive
System.out.println(rex instanceof Animal); // true — transitive
System.out.println(rex instanceof Object); // true — always
// ── All inherited methods available on Dog ────────────────────────────
rex.bark(); // Dog method
rex.nurseYoung(); // Mammal method
rex.breathe(); // Animal method
rex.eat("bone"); // Animal method
rex.regulateTemperature(); // Mammal method
// ── Polymorphism — Dog used at any level in the chain ─────────────────
Animal a = rex; Mammal m = rex; Dog d = rex;
a.breathe(); m.nurseYoung(); d.bark();Constructor Execution Order
Constructor execution in a multilevel chain follows strict top-down order. When new Dog() is called, the Dog constructor runs first — but its first action is calling super(), which triggers the Mammal constructor. Mammal's first action is also calling super(), which triggers the Animal constructor. Animal's implicit super() calls Object(). Only after Object() completes does Animal's constructor body run, then Mammal's body, then Dog's body. The result is bottom-up in terms of who initiates the call, but top-down in terms of who runs first.
This ordering guarantees that when a constructor body executes, the entire superclass portion of the object is already fully initialised. Animal's constructor finishes before Mammal's body runs, so Mammal's code can safely rely on everything Animal initialised. This is critical when a method call in a constructor could reach down to a subclass method that depends on subclass fields — the top-down initialisation order prevents partially-initialised state from being exposed.
The compiler enforces that every constructor either explicitly calls super() or this() as its first statement, or has super() inserted implicitly. If the direct superclass has no accessible no-argument constructor, the compiler forces an explicit super(...) call. This is why adding a parameterised constructor to a superclass without also keeping a no-argument constructor can cause compile errors throughout the subclass hierarchy.
Java
// ── Constructor execution order visualised ───────────────────────────
public class LevelA {
private final String dataA;
public LevelA(String dataA) {
System.out.println("1. LevelA constructor — before assignment");
this.dataA = dataA;
System.out.println("2. LevelA constructor — after assignment dataA=" + dataA);
}
protected String getDataA() { return dataA; }
}
public class LevelB extends LevelA {
private final String dataB;
public LevelB(String dataA, String dataB) {
super(dataA); // step 1: LevelA constructor runs FIRST
System.out.println("3. LevelB constructor — before assignment");
this.dataB = dataB;
System.out.println("4. LevelB constructor — dataA=" +
getDataA() + " dataB=" + dataB);
}
protected String getDataB() { return dataB; }
}
public class LevelC extends LevelB {
private final String dataC;
public LevelC(String dataA, String dataB, String dataC) {
super(dataA, dataB); // step 2: LevelB constructor (→ LevelA first)
System.out.println("5. LevelC constructor — before assignment");
this.dataC = dataC;
System.out.println("6. LevelC fully constructed: " +
getDataA() + ", " + getDataB() + ", " + dataC);
}
}
// ── Output when new LevelC("A","B","C") runs ─────────────────────────
// 1. LevelA constructor — before assignment
// 2. LevelA constructor — after assignment dataA=A
// 3. LevelB constructor — before assignment
// 4. LevelB constructor — dataA=A dataB=B
// 5. LevelC constructor — before assignment
// 6. LevelC fully constructed: A, B, C
// ── When superclass lacks no-arg constructor ──────────────────────────
public class Parent {
private final int value;
public Parent(int value) { this.value = value; }
// No Parent() — every subclass constructor must call super(int)
}
public class Child extends Parent {
public Child() {
// super(); // COMPILE ERROR — Parent has no no-arg constructor
super(0); // MUST provide the required argument
}
public Child(int value) {
super(value); // explicit call required
}
}Method Resolution in Multilevel Chains
When a method is called on an object, Java uses the actual runtime type of the object — not the declared type of the reference — to find the method implementation. The JVM walks up the inheritance chain from the most derived class toward Object, using the first implementation it finds. This is the method resolution order for virtual (non-static, non-private, non-final) methods.
This search means that if Dog overrides breathe(), calling breathe() on a Dog — even through an Animal reference — always calls Dog's version. If Dog does not override breathe() but Mammal does, Mammal's version is used. If neither overrides it, Animal's version is used. The rule is simple: the most specific (lowest in the chain) implementation wins.
Static methods are resolved differently — at compile time based on the declared type of the reference, not the runtime type. This is called static binding or hiding. If Animal declares a static utility method and Dog declares a static method with the same signature, they are two independent methods. Calling via an Animal reference calls Animal's version; calling via a Dog reference calls Dog's version. This is not overriding — it is hiding — and the @Override annotation will cause a compile error if applied to a static method hiding another static method.
Java
// ── Virtual method resolution — most specific wins ───────────────────
public class A {
public String greet() { return "Hello from A"; }
public String info() { return "Info from A"; } // not overridden
public String identify(){ return "A"; }
}
public class B extends A {
@Override
public String greet() { return "Hello from B"; } // overrides A
// info() NOT overridden — A's version inherited
@Override
public String identify(){ return "B → " + super.identify(); }
}
public class C extends B {
@Override
public String greet() { return "Hello from C"; } // overrides B (and A)
// info() still not overridden — A's version still used
@Override
public String identify(){ return "C → " + super.identify(); }
}
C c = new C();
A a = c; // widening — declared type A, runtime type C
System.out.println(c.greet()); // "Hello from C" — C's override
System.out.println(a.greet()); // "Hello from C" — runtime type wins!
System.out.println(c.info()); // "Info from A" — no override in B or C
System.out.println(a.info()); // "Info from A" — same result
System.out.println(c.identify()); // "C → B → A" — super chain
// ── super.method() reaches exactly one level up ───────────────────────
// C.identify() calls super.identify() → B.identify()
// B.identify() calls super.identify() → A.identify()
// Result: "C → B → A"
// ── Static method hiding — NOT overriding ────────────────────────────
class Base {
public static String staticMethod() { return "Base.static"; }
public String virtualMethod(){ return "Base.virtual"; }
}
class Derived extends Base {
// Hides Base.staticMethod — NOT an override
public static String staticMethod() { return "Derived.static"; }
@Override
public String virtualMethod(){ return "Derived.virtual"; }
}
Base d = new Derived();
System.out.println(d.staticMethod()); // "Base.static" — compile-time type
System.out.println(d.virtualMethod()); // "Derived.virtual" — runtime type
// Static hiding: reference type determines which static method is called
// Virtual dispatch: actual object type determines which instance method runsDeep Hierarchies and Their Costs
Multilevel inheritance is powerful, but every additional level in the chain adds cognitive complexity. Reading a class at the bottom of a deep hierarchy requires understanding every class above it — a developer cannot understand Dog without reading Mammal, Animal, and Object. Each level is a dependency, and each dependency is something that can change and break the level below it. This is the fragile base class problem: a change deep in the hierarchy (in Animal, say) can have cascading effects on every class below it, even those many levels down.
The fragile base class problem is most acute with inheritance of implementation. A subclass that extends a concrete superclass is exposed to every implementation decision the superclass makes. If Animal's eat() method calls a private helper that changes state, every subclass inherits that hidden coupling. If Animal adds new instance variables, they are added to every subclass object's memory footprint.
Practical guidelines emerge from understanding these costs. Prefer shallow hierarchies — two or three levels is usually sufficient for most domains. Make superclasses abstract or final: abstract enforces that the class is only used as a base, not instantiated directly; final prevents further extension. Prefer programming to interfaces over programming to abstract classes, which provides the polymorphism benefits of inheritance without the implementation coupling. When a hierarchy grows beyond three or four levels, it is almost always a signal that composition should replace some of the inheritance.
Java
// ── Fragile base class problem ───────────────────────────────────────
public class BaseList<E> {
private int modCount = 0;
public void add(E element) {
doAdd(element);
modCount++; // ← subclasses do not know this happens
}
public void addAll(List<E> items) {
for (E item : items) {
add(item); // ← calls add() which increments modCount
}
}
protected void doAdd(E element) { /* underlying storage */ }
}
// ── Subclass that counts additions — breaks due to base class impl ────
public class CountingList<E> extends BaseList<E> {
private int addCount = 0;
@Override
public void add(E element) {
addCount++; // count before delegating to super
super.add(element);
}
@Override
protected void doAdd(E element) { /* own storage */ }
public int getAddCount() { return addCount; }
}
CountingList<String> list = new CountingList<>();
list.addAll(List.of("a", "b", "c"));
// Expected: addCount = 3
// Actual: addCount = 3 ... only if BaseList.addAll calls doAdd directly
// If BaseList.addAll calls add(), then CountingList.add() is called,
// addCount increments 3 times. Correct only by accident.
// If BaseList later refactors addAll to call doAdd directly,
// CountingList silently breaks. This is the fragile base class problem.
// ── Prefer shallow hierarchies ────────────────────────────────────────
// TOO DEEP — hard to understand, fragile
// Object → Entity → NamedEntity → Person → Employee → Manager
// → ContractEmployee → SeniorContractEmployee
// BETTER — two levels maximum per domain concept
// abstract Employee (no-arg constructor blocked, abstract methods)
// → FullTimeEmployee
// → ContractEmployee
// Combine through composition for seniority, department, etc.
// ── final prevents further extension ─────────────────────────────────
public final class SSN { // Social Security Number — never extend
private final String value;
public SSN(String value) {
if (!value.matches("\d{3}-\d{2}-\d{4}"))
throw new IllegalArgumentException("Invalid SSN format");
this.value = value;
}
public String getValue() { return value; }
@Override public String toString() { return "***-**-" + value.substring(7); }
}Progressive Specialisation — A Complete Example
The value of multilevel inheritance is progressive specialisation — each level adds precisely the concepts that belong at its level of abstraction without duplicating what belongs above. A well-designed multilevel hierarchy is a precise model of the domain, where each class's responsibilities are clear and non-overlapping. The hierarchy should read naturally: every concept in a class genuinely applies to all instances of that class and to all subclasses.
Java
// ── Progressive specialisation: Employee hierarchy ────────────────────
// Person → Employee → Manager
public class Person {
private final String firstName;
private final String lastName;
private final LocalDate dateOfBirth;
public Person(String firstName, String lastName,
LocalDate dateOfBirth) {
this.firstName = firstName;
this.lastName = lastName;
this.dateOfBirth = dateOfBirth;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public LocalDate getDateOfBirth() { return dateOfBirth; }
public String getFullName() {
return firstName + " " + lastName;
}
public int getAge() {
return Period.between(dateOfBirth, LocalDate.now()).getYears();
}
@Override
public String toString() {
return getFullName() + " (age " + getAge() + ")";
}
}
public class Employee extends Person {
private final String employeeId;
private final LocalDate hireDate;
private BigDecimal salary;
private String department;
public Employee(String firstName, String lastName,
LocalDate dateOfBirth, String employeeId,
BigDecimal salary, String department) {
super(firstName, lastName, dateOfBirth);
this.employeeId = employeeId;
this.hireDate = LocalDate.now();
this.salary = salary;
this.department = department;
}
public int getYearsOfService() {
return Period.between(hireDate, LocalDate.now()).getYears();
}
public void raiseSalary(double percentIncrease) {
if (percentIncrease < 0 || percentIncrease > 100)
throw new IllegalArgumentException(
"Invalid raise percentage: " + percentIncrease);
salary = salary.multiply(
BigDecimal.valueOf(1 + percentIncrease / 100));
}
public BigDecimal getSalary() { return salary; }
public String getEmployeeId() { return employeeId; }
public String getDepartment() { return department; }
@Override
public String toString() {
return super.toString() +
" [" + employeeId + ", " + department + "]";
}
}
public class Manager extends Employee {
private final List<Employee> directReports;
private BigDecimal budgetAuthority;
public Manager(String firstName, String lastName,
LocalDate dateOfBirth, String employeeId,
BigDecimal salary, String department,
BigDecimal budgetAuthority) {
super(firstName, lastName, dateOfBirth,
employeeId, salary, department);
this.directReports = new ArrayList<>();
this.budgetAuthority = budgetAuthority;
}
public void addDirectReport(Employee employee) {
directReports.add(employee);
}
public void conductReview(Employee employee, String rating) {
if (!directReports.contains(employee))
throw new IllegalArgumentException(
employee.getFullName() + " is not a direct report");
System.out.println("Review for " + employee.getFullName() +
": " + rating);
}
public int getTeamSize() { return directReports.size(); }
public BigDecimal getBudgetAuthority(){ return budgetAuthority; }
public List<Employee> getDirectReports(){
return Collections.unmodifiableList(directReports);
}
@Override
public String toString() {
return super.toString() +
" [Manager, team=" + directReports.size() + "]";
}
}
// ── Using the hierarchy ───────────────────────────────────────────────
Manager alice = new Manager(
"Alice", "Smith", LocalDate.of(1980, 3, 15),
"EMP001", new BigDecimal("120000"), "Engineering",
new BigDecimal("500000"));
Employee bob = new Employee(
"Bob", "Jones", LocalDate.of(1990, 7, 22),
"EMP002", new BigDecimal("90000"), "Engineering");
alice.addDirectReport(bob);
// All three levels of behaviour accessible
System.out.println(alice.getFullName()); // Person method
System.out.println(alice.getYearsOfService()); // Employee method
alice.conductReview(bob, "Exceeds Expectations"); // Manager method
// Polymorphism — Alice is an Employee and a Person
List<Person> people = List.of(alice, bob);
List<Employee> employees = List.of(alice, bob);
// Both valid — alice IS-A Person, IS-A Employee, IS-A ManagerRelated Topics in Object-Oriented Programming
OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.