Inheritance
Inheritance lets one class build on another. The new class (the subclass, or child) automatically receives the fields and methods of an existing class (the superclass, or parent), and can add new members or change inherited behavior. It models "is-a" relationships, such as a manager is an employee and a savings account is an account, and lets shared code be written once.
Basic Syntax: extends
public class Vehicle {
protected final String registration;
protected int speed;
public Vehicle(String registration) {
this.registration = registration;
}
public void accelerate(int amount) {
speed += amount;
}
public String describe() {
return registration + " moving at " + speed + " km/h";
}
}
public class Truck extends Vehicle {
private final double maxLoadTons;
private double currentLoad;
public Truck(String registration, double maxLoadTons) {
super(registration);
this.maxLoadTons = maxLoadTons;
}
public void load(double tons) {
if (currentLoad + tons > maxLoadTons) {
throw new IllegalStateException("overloaded");
}
currentLoad += tons;
}
}
Truck t = new Truck("TN-09-AB-1234", 12);
t.load(8); // defined in Truck
t.accelerate(40); // inherited from Vehicle
System.out.println(t.describe()); // inherited: TN-09-AB-1234 moving at 40 km/h
Truck extends Vehicle means a Truck has everything a Vehicle has (registration, speed, accelerate, describe) plus what Truck adds (maxLoadTons, currentLoad, load). The code for accelerating and describing exists only in Vehicle, and every kind of vehicle reuses it.
What Is Inherited
A subclass inherits all non-private members of its superclass: public and protected fields and methods, and package-private ones when both classes are in the same package.
| Member in the superclass | Accessible in the subclass? |
|---|---|
public | Yes |
protected | Yes, even from a different package |
| package-private (no modifier) | Only if the subclass is in the same package |
private | No (it still exists in the object, but cannot be named) |
| Constructors | Never inherited |
static members | Accessible, but belong to the class, not inherited per object |
Private fields deserve a closer look. A Truck object really does contain the private fields declared in Vehicle; they are part of its memory. Truck's code simply cannot access them by name. It must use the methods Vehicle provides, which keeps Vehicle in control of its own data:
public class Vehicle {
private int speed;
public int getSpeed() { return speed; }
public void accelerate(int amount) { speed += amount; }
}
public class Truck extends Vehicle {
public void report() {
System.out.println(speed); // compile error: speed has private access
System.out.println(getSpeed()); // fine
}
}
protected fields are convenient, but they let every subclass bypass the parent's validation. Private fields with protected or public methods are usually safer.
Constructors and super(...)
Constructors are not inherited. Each subclass constructor must make sure the superclass part of the object is initialized, by calling a superclass constructor with super(...):
public Truck(String registration, double maxLoadTons) {
super(registration); // initialize the Vehicle part
this.maxLoadTons = maxLoadTons; // then the Truck part
}
If a constructor does not call super(...) explicitly, the compiler inserts super(), a call to the parent's no-argument constructor. When the parent has no such constructor, compilation fails:
public class Truck extends Vehicle {
public Truck() { } // compile error: constructor Vehicle in class Vehicle cannot be applied to given types
}
Construction always proceeds from the top of the hierarchy down: Object, then Vehicle, then Truck. A subclass can rely on its parent being fully initialized by the time its own constructor body runs.
Overriding Methods
A subclass can provide its own version of an inherited method, with the same name, parameters, and a compatible return type. This is overriding:
public class Truck extends Vehicle {
// ...
@Override
public String describe() {
return super.describe() + ", carrying " + currentLoad + "/" + maxLoadTons + " t";
}
}
System.out.println(t.describe());
// TN-09-AB-1234 moving at 40 km/h, carrying 8.0/12.0 t
super.describe() calls the parent's version, and the override adds to its result rather than duplicating it. The @Override annotation makes the compiler verify that a matching parent method exists; a typo such as descibe() would otherwise silently create a new, unrelated method. Overriding has its own lesson.
Inheritance Creates an "is-a" Relationship
A subclass object can be used wherever its superclass type is expected:
Vehicle v = new Truck("KA-01-XY-9876", 10); // a Truck is a Vehicle
v.accelerate(30);
System.out.println(v.describe()); // runs Truck's describe()
List<Vehicle> fleet = List.of(
new Vehicle("TN-01-AA-0001"),
new Truck("TN-09-AB-1234", 12)
);
for (Vehicle each : fleet) {
System.out.println(each.describe());
}
The variable's declared type, Vehicle, determines which methods can be called: v.load(5) does not compile, because not every Vehicle has load. The object's actual class, Truck, determines which implementation runs. This is polymorphism, the subject of the next lesson.
The Object Class
Every class that does not explicitly extend another implicitly extends java.lang.Object. So every class inherits:
toString()equals(Object)andhashCode()getClass()wait(),notify(),notifyAll()(thread coordination)
This is why any object can be printed, put in a collection, or compared with equals, and why these methods are so often overridden.
Single Inheritance and Hierarchies
A Java class extends exactly one superclass. Multiple inheritance of classes is not allowed:
public class AmphibiousCar extends Car, Boat { } // compile error
Allowing two parents would create ambiguity whenever both define the same method or field (the "diamond problem"). Java instead allows a class to implement any number of interfaces, which provide multiple inheritance of type without inheriting conflicting state.
Inheritance can extend over several levels:
Object
└── Vehicle
├── Car
│ └── ElectricCar
└── Truck
ElectricCar inherits from Car, which inherits from Vehicle, which inherits from Object. Deep hierarchies become hard to understand, since behavior may be spread across many levels. Two or three levels below Object is usually plenty.
Preventing Inheritance
- A
finalclass cannot be extended (String,Integer,LocalDate). - A
finalmethod cannot be overridden. - A
sealedclass (Java 17+) restricts which classes may extend it:
public sealed class Payment permits CardPayment, UpiPayment, CashPayment { }
public final class CardPayment extends Payment { }
public final class UpiPayment extends Payment { }
public final class CashPayment extends Payment { }
Only the three listed classes may extend Payment. Each permitted subclass must declare itself final, sealed, or non-sealed. The compiler then knows the complete set of subtypes, which allows exhaustive switch expressions over them without a default branch.
When to Use Inheritance
Use inheritance when:
- There is a genuine "is-a" relationship: every
Truckreally is aVehicleand can be used anywhere aVehicleis expected. - Subclasses share real behavior with the parent, not just a few fields.
- The parent class was designed to be extended.
Prefer composition ("has-a") when the relationship is about using another object's capabilities:
// Inheritance misused: a Stack is not a kind of ArrayList
public class Stack<T> extends ArrayList<T> { ... }
// Callers can now call add(0, x) or remove(5), breaking stack behavior
// Composition: a Stack has a list it uses internally
public class Stack<T> {
private final List<T> items = new ArrayList<>();
public void push(T item) { items.add(item); }
public T pop() { return items.remove(items.size() - 1); }
public boolean isEmpty() { return items.isEmpty(); }
}
The inheritance version exposes every ArrayList method, so callers can insert into the middle of the "stack." The composition version exposes only stack operations. The JDK's own java.util.Stack extends Vector and suffers from exactly this problem, which is one reason ArrayDeque is recommended instead.
A useful test: if a subclass needs to disable or work around inherited methods, inheritance is the wrong tool.
Practical Example: Employee Payroll
import java.util.List;
public class Payroll {
static class Employee {
private final String name;
private final double baseMonthly;
Employee(String name, double baseMonthly) {
this.name = name;
this.baseMonthly = baseMonthly;
}
public String getName() { return name; }
protected double getBaseMonthly() { return baseMonthly; }
public double monthlyPay() {
return baseMonthly;
}
@Override
public String toString() {
return String.format("%-8s %-10s %10.2f",
getClass().getSimpleName(), name, monthlyPay());
}
}
static class Manager extends Employee {
private final double teamBonus;
Manager(String name, double baseMonthly, double teamBonus) {
super(name, baseMonthly);
this.teamBonus = teamBonus;
}
@Override
public double monthlyPay() {
return super.monthlyPay() + teamBonus;
}
}
static class Intern extends Employee {
Intern(String name, double stipend) {
super(name, stipend);
}
@Override
public double monthlyPay() {
return getBaseMonthly() * 0.9; // 10% held for course fees
}
}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("Ravi", 60_000),
new Manager("Meena", 95_000, 15_000),
new Intern("Arjun", 20_000)
);
double total = 0;
for (Employee e : staff) {
System.out.println(e);
total += e.monthlyPay();
}
System.out.printf("Total payroll: %.2f%n", total);
}
}
Output:
Employee Ravi 60000.00
Manager Meena 110000.00
Intern Arjun 18000.00
Total payroll: 188000.00
Manager and Intern inherit the name, base pay, and toString from Employee; each adds only what is different. Manager.monthlyPay builds on the parent's calculation through super.monthlyPay(). Intern reaches the private base amount through the protected getter rather than the field. The payroll loop treats every person as an Employee, yet each object computes its own pay, and toString (written once in Employee) automatically reports the correct class name and amount through getClass() and the overridden monthlyPay().