Inheritance, Polymorphism, and Method Overloading
So far every class you wrote lived on its own. But in any real system you run into classes that share a good chunk of their state and behavior: a Car, a Motorcycle, and a Truck all have a brand, they all start and they all brake.
Copy-pasting those members into three classes works right up until a rule changes. Then you have to remember all three places. You already know how that ends.
Inheritance solves that problem, and polymorphism — which is its consequence, not a separate topic — is what makes it worth having. This lesson covers both together, because apart they do not make sense.
1. Inheritance: extends and the “is-a” test
A class can extend another and automatically receive all of its fields and methods:
public class Vehicle {
protected String brand;
public Vehicle(String brand) {
this.brand = brand;
}
public void start() {
System.out.println("The vehicle starts.");
}
public void brake() {
System.out.println("The vehicle brakes.");
}
}
public class Car extends Vehicle {
public Car(String brand) {
super(brand);
}
@Override
public void start() {
System.out.println("Car " + brand + " starting with a push button.");
}
public void openTrunk() {
System.out.println("Trunk open.");
}
}
Car declares neither brand nor brake(), yet it has both. It inherited them.
The test to run before writing extends
Before inheriting, say this out loud: “is an X an Y?”
- A
Caris aVehicle. ✔ Inheritance is right. - A
Caris anEngine. ✘ A car has an engine. That is composition, not inheritance.
If the sentence sounds odd, the inheritance is wrong. And a wrong inheritance does not show up on day one: it shows up six months later, when the subclass has inherited five methods that make no sense in it.
Java has single inheritance: a class extends exactly one class. There is no
extends A, B. To combine behavior from several sources you use interfaces, which you will see in the next lesson.
2. super: the constructor chain
This is the part that confuses people most at the start. When you instantiate a subclass, it does not run one constructor: it runs the whole chain, from the most distant ancestor down to the concrete class.
Three rules the compiler enforces without exception:
super(...)must be the first statement in the constructor. Same asthis(...), and for the same reason: nothing may run before the inherited part is ready.- If you do not write
super(...), Java inserts a no-argumentsuper()automatically. - If the superclass has no no-argument constructor, that automatic insertion fails and the compiler forces you to explicitly call one that does exist.
This is the single most common error in the whole lesson:
public class Vehicle {
protected String brand;
public Vehicle(String brand) { this.brand = brand; }
// By writing this constructor, Vehicle no longer has a no-arg one
}
public class Car extends Vehicle {
public Car() {
// ERROR: Java tries to insert super() and Vehicle has no such constructor
}
}
super also lets you call the parent’s version of a method you are overriding, which is very common when you want to extend behavior rather than replace it:
@Override
public void start() {
super.start(); // first do what every vehicle does
System.out.println("...and engage keyless ignition.");
}
3. Overriding: @Override and the contract you cannot break
Overriding means redefining, in the subclass, a method that already exists in the superclass, with the same signature: same name, same parameter types, in the same order.
The @Override annotation is not mandatory, but write it every time. It changes nothing at runtime; what it does is ask the compiler to verify you are actually overriding something:
public class Car extends Vehicle {
@Override
public void start(int speed) { // ← compile error, and that is a good thing
...
}
}
Without @Override, that method would compile perfectly. Java would treat it as a new Car method named start taking an int, and the original start() would remain inherited and untouched. Your code would run, would not do what you expected, and there would be no error to guide you. @Override turns a silent bug into a compile error.
What the subclass may and may not change
| Element | Rule when overriding |
|---|---|
| Name and parameters | Identical. If they change, it is a new method, not an override. |
| Return type | The same, or a subtype of the original (covariant return). |
| Visibility | The same or wider. A public method cannot become protected. |
| Checked exceptions | The same, fewer, or subtypes. Never anything broader. |
private, static, or final methods | Cannot be overridden at all. |
The visibility rule has a very concrete logic: if anyone can treat a Car as a Vehicle, and Vehicle.start() is public, then start() must remain callable on the Car. Narrowing it would break that promise.
4. Overloading and overriding: similar names, different concepts
These two words get mixed up constantly, and the underlying difference is when the decision about which method runs is made.
public class Console {
public void print(String text) { ... }
public void print(int number) { ... }
public void print(String text, int times) { ... }
}
None of this is inheritance. It is simply convenience: three ways to call something that is conceptually one operation.
Return type does not count for overloading.
int compute()anddouble compute()in the same class will not compile: the compiler has no way to decide which one you meant when you write a barecompute();.
5. Polymorphism and dynamic dispatch
This is where everything above comes together. A variable declared as Vehicle can point at any object that is a Vehicle, including instances of its subclasses:
Vehicle v = new Car("Toyota");
v.start(); // Prints: "Car Toyota starting with a push button."
Notice what happened: the variable says Vehicle, but Car’s code ran. That is polymorphism, and the mechanism is called dynamic dispatch.
What it is actually for
The value of polymorphism is not in one isolated call — it is in being able to write code that does not know which subclass it is working with, and does not care:
public class Garage {
// This method knows nothing about Car, Motorcycle, or Truck. It does not need to.
public void inspect(Vehicle[] fleet) {
for (Vehicle v : fleet) {
v.start(); // each object runs ITS own version
v.brake();
}
}
}
Vehicle[] fleet = {
new Car("Toyota"),
new Motorcycle("Honda"),
new Truck("Scania")
};
new Garage().inspect(fleet);
Tomorrow you add a Bicycle extends Vehicle class and Garage handles it without you touching a single line of it. That is the payoff: new code that plugs in without modifying code that already worked.
The alternative without polymorphism is this chain, which grows forever and has to be edited every time:
// The code polymorphism saves you from writing
if (v instanceof Car) {
((Car) v).startCar();
} else if (v instanceof Motorcycle) {
((Motorcycle) v).startMotorcycle();
} else if (v instanceof Truck) {
...
}
Casting and instanceof
With a Vehicle reference you may only call what Vehicle declares. If you need something specific to the subclass you have to cast — and cast with a safety net:
Vehicle v = new Car("Toyota");
// v.openTrunk(); // ERROR: Vehicle does not declare openTrunk()
if (v instanceof Car car) { // pattern matching, since Java 16
car.openTrunk(); // 'car' arrives already cast and ready
}
Without the check, a cast to the wrong type blows up at runtime with ClassCastException. And if you find yourself casting often, take it as a signal: the method you need should probably be declared on the superclass.
6. When NOT to inherit
Inheritance is the strongest relationship two classes can have: the subclass is tied to the parent’s internal details forever. Every change in the superclass can break subclasses nobody touched.
That is why the industry rule is prefer composition over inheritance:
// Forced inheritance: is a Car AN Engine? No.
public class Car extends Engine { ... }
// Composition: a Car HAS AN engine. This one holds.
public class Car {
private final Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.ignite(); // delegates to the engine
}
}
Composition lets you swap the engine without touching the car, and test the car with a fake engine. Inheritance lets you do neither.
When a class must not be extended, say so with final and let the compiler enforce it:
public final class Coordinate { ... } // nobody can inherit from this
public class Account {
public final void credit(double amount) { ... } // this method cannot be overridden
}
7. Everything inherits from Object
Even when you never write extends, every Java class inherits from Object. That is where methods you have already used without noticing come from:
public class Car extends Vehicle {
@Override
public String toString() {
return "Car{brand='" + brand + "'}";
}
}
Car c = new Car("Toyota");
System.out.println(c); // Java calls toString() on its own
Without overriding toString(), System.out.println(c) prints something like Car@1b6d3586: the class name and a hash code. Useless for debugging. Overriding it costs two lines and gives you back hours.
Object also brings equals() and hashCode(), which have rules of their own and plenty of traps. You will cover them in depth in the lesson on iterators and ordering.
8. Common mistakes
| Mistake | What happens | How to fix it |
|---|---|---|
| Overriding while changing the parameters | Java treats it as a brand-new method. The original stays inherited and your code does none of what you expected. | Always write @Override: it turns the bug into a compile error. |
Subclass constructor with no super(...) when the parent has no empty constructor | A confusing compile error about a constructor you never wrote. | Explicitly call super(arguments) on the first line. |
| Calling an overridable method from the parent’s constructor | The subclass version runs before its fields are initialized: unexplained null or 0 values. | Have constructors call only private or final methods. |
Casting without checking with instanceof | ClassCastException at runtime. | Use if (v instanceof Car car), or rethink why you need the cast at all. |
| Inheriting just to reuse code, with no real “is-a” | Rigid hierarchies where the subclass inherits meaningless methods. | Compose: hold the object as a field and delegate to it. |
| Confusing overloading with overriding | You expect polymorphism and get a static selection made by the compiler. | Overloading: same class, different signatures. Overriding: subclass, same signature. |
9. Guided hands-on exercise
Challenge: an employee hierarchy
- Create an
Employeesuperclass withnameandbaseSalary(private with getters), a constructor validating that the salary is not negative, and acomputeSalary()method returning the base salary. - Create
Manager extends Employee, adding abonusand overridingcomputeSalary()to add it. - Create
SalesRep extends Employee, withmonthlySalesand an 8% commission. - Override
toString()in all three. - In
main, build anEmployee[]holding objects of all three types, iterate it once, and print each salary. The loop must not useinstanceofand must not cast.
See suggested solution
public class Employee {
private final String name;
private final double baseSalary;
public Employee(String name, double baseSalary) {
if (name == null || name.isBlank()) {
System.out.println("Invalid name, used \"No name\" by default.");
name = "No name";
}
if (baseSalary < 0) {
System.out.println("Invalid base salary, used 0 by default.");
baseSalary = 0;
}
this.name = name;
this.baseSalary = baseSalary;
}
public String getName() { return name; }
public double getBaseSalary() { return baseSalary; }
public double computeSalary() {
return baseSalary;
}
@Override
public String toString() {
return getClass().getSimpleName() + " " + name;
}
}
public class Manager extends Employee {
private final double bonus;
public Manager(String name, double baseSalary, double bonus) {
super(name, baseSalary); // first statement, mandatory
if (bonus < 0) {
System.out.println("Invalid bonus, used 0 by default.");
bonus = 0;
}
this.bonus = bonus;
}
@Override
public double computeSalary() {
return super.computeSalary() + bonus; // extends, does not replace
}
}
public class SalesRep extends Employee {
private static final double COMMISSION = 0.08;
private final double monthlySales;
public SalesRep(String name, double baseSalary, double monthlySales) {
super(name, baseSalary);
if (monthlySales < 0) {
System.out.println("Invalid sales, used 0 by default.");
monthlySales = 0;
}
this.monthlySales = monthlySales;
}
@Override
public double computeSalary() {
return super.computeSalary() + monthlySales * COMMISSION;
}
}
public class MainPayroll {
public static void main(String[] args) {
Employee[] payroll = {
new Employee("Ana Torres", 800000),
new Manager("Luis Paz", 1500000, 400000),
new SalesRep("Sofia Rios", 700000, 2500000)
};
double total = 0;
// One loop, no instanceof and no casts:
for (Employee e : payroll) {
double salary = e.computeSalary();
total += salary;
System.out.printf("%-22s $ %,.2f%n", e, salary);
}
System.out.printf("%-22s $ %,.2f%n", "TOTAL", total);
}
}
The thing to look at here is the loop. It never asks what type each employee is, and yet each one computes its salary its own way. If tomorrow you add Intern extends Employee, that loop keeps working without a single edit. That is polymorphism doing its job.
Notice super.computeSalary() too: Manager and SalesRep do not repeat the base-salary logic, they reuse it and add their own part on top.
Key takeaways
extendsis only justified when the sentence “an X is a Y” is true. Otherwise, compose.- The constructor chain goes up via
super(...)and executes coming down: the parent is always initialized before the child. - Write
@Overrideevery time: it turns a silent phantom method into a compile error. - Overloading = same class, different signatures, decided by the compiler. Overriding = subclass, same signature, decided by the JVM.
- The variable defines what you can call; the object defines what runs. That is the whole of polymorphism.
- The real benefit is writing code that works with subclasses that do not exist yet.
- Frequent casting is a symptom that the hierarchy is asking for a method on the superclass.
- Prefer composition over inheritance, and mark with
finalwhatever must not be extended.