Extending a class instead of copying it
When two classes share fields and behaviour, copying that code into both is a maintenance trap. Inheritance lets one class say "I am everything a Student is, plus a little more."
After this lesson
You should be able to
- Write a subclass using extends and explain what it automatically inherits.
- Explain substitutability: why a subclass object can be used wherever the base class is expected.
- Name at least one real cost of inheritance, not just its benefits.
The problem: two classes, one shared shape
Imagine a Student class with a name and roll number, and a Teacher class that also needs a name and an ID. Without inheritance, you write name-handling code twice, in two separate classes — and if you later add validation to how names are stored, you must remember to fix it in both places. This is the same duplication risk that motivated functions in CS105ES, now at the level of an entire class.
A base class, sometimes called a superclass, holds the fields and methods common to both. Person can hold name; Student and Teacher can each extend Person and add only what makes them different. This is what "is-a" means in object orientation: a Student is a Person, in the precise sense that everything true of a Person is also true of a Student.
class Person {
protected String name;
public Person(String name) {
this.name = name;
}
public void display() {
System.out.println("Name: " + name);
}
}
class Student extends Person {
private int rollNumber;
public Student(String name, int rollNumber) {
super(name); // build the Person part first
this.rollNumber = rollNumber;
}
}Substitutability: the actual payoff
Because a Student is a Person, a Student object can be used anywhere a Person is expected — passed to a method that takes a Person parameter, stored in an array of Person, called through a Person reference. This is substitutability, and it is not a side effect of inheritance, it is the entire reason inheritance is useful for writing general code: a method written once for Person works correctly for every kind of Person that will ever be invented, including ones that do not exist yet when the method is written.
This is what lets a printing function written for Person automatically work for Student and Teacher without modification — you extend the family of types, and every piece of code already written for the base class keeps working for the new subclass, with zero changes to that existing code.
Six shapes inheritance can take, and one real cost
Inheritance is used in more than one way, and naming the pattern helps you judge whether it fits: specialization (a Student is a more specific kind of Person), specification (a class defines only method signatures for subclasses to fill in — close to what an abstract class or interface does), construction (reusing implementation even where the is-a relationship is a little loose), extension (adding genuinely new behaviour, like a PriorityQueue extending Queue), limitation (a subclass that restricts what the base class allows), and combination (inheriting from more than one source of behaviour, which Java restricts — more on that with interfaces).
The real cost of inheritance is coupling: a subclass depends on the internal decisions of its base class, so changing the base class can silently break every subclass, even ones the base class's author never saw. This is why inheritance is described as a strong relationship, to be chosen deliberately, not the default way to reuse code whenever two classes happen to share a few fields.
Try it yourself
Design a Vehicle base class with a speed field and a method accelerate(). Extend it with a Car subclass that adds a numberOfDoors field. Write a method printInfo(Vehicle v) that works correctly whether it is called with a Vehicle or a Car.
Need a hint?
printInfo only needs to know about Vehicle's fields and methods — that is exactly what substitutability guarantees it can rely on, regardless of which actual subclass is passed in.
Check the worked solution
printInfo(Vehicle v) works for a Car argument without any special case, because a Car is-a Vehicle and substitutability guarantees that anything true of a Vehicle parameter is true of the Car passed to it. This is the concrete payoff promised earlier in the lesson, not an abstract claim.
class Vehicle {
protected int speed = 0;
public void accelerate() {
speed += 10;
}
}
class Car extends Vehicle {
private int numberOfDoors;
public Car(int numberOfDoors) {
this.numberOfDoors = numberOfDoors;
}
}
static void printInfo(Vehicle v) {
System.out.println("Speed: " + v.speed);
}
// printInfo(new Car(4)); // works — a Car is a VehicleQuick check
Why can a method written to accept a Vehicle parameter also correctly accept a Car argument?
Why this lesson exists
Syllabus mapping
Hierarchical abstractions, Base class object, subclass, subtype, substitutability, forms of inheritance specialization, specification, construction, extension, limitation, combination, benefits of inheritance, costs of inheritance.
Maps to course outcome CO2.