Inheritance & Polymorphism
Many things in the real world are special versions of other things. A manager is an employee with extra responsibilities. A car, a truck and an auto-rickshaw are all vehicles. Inheritance lets one class build on another: the new class gets all the fields and methods of the existing one and adds or changes what it needs.
Polymorphism (Greek for "many forms") is what makes inheritance powerful: code written for the general type, such as Vehicle, automatically works with every special type, and each object behaves in its own way. In this lesson you will use both, and learn how to cast safely with instanceof patterns.
extends and super
A subclass (child class) declares its parent with extends. The parent is called the superclass. The subclass inherits the parent's accessible fields and methods, and its constructor must first run the parent's constructor with super(...).
public class Staff {
public static void main(String[] args) {
Employee e = new Employee("Rohan", 42000);
Manager m = new Manager("Priya", 85000, 15000);
System.out.println(e.describe());
System.out.println(m.describe());
System.out.println(m.getName() + " leads a team: " + m.hasTeam());
System.out.println("Priya's monthly pay: Rs " + m.monthlyPay());
}
}
class Employee {
protected final String name;
protected final double salary;
Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
String getName() { return name; }
double monthlyPay() {
return salary;
}
String describe() {
return name + " earns Rs " + monthlyPay();
}
}
class Manager extends Employee {
private final double allowance;
Manager(String name, double salary, double allowance) {
super(name, salary); // run Employee's constructor first
this.allowance = allowance;
}
boolean hasTeam() { return true; }
@Override
double monthlyPay() {
return super.monthlyPay() + allowance;
}
@Override
String describe() {
return "Manager " + super.describe();
}
}Rohan earns Rs 42000.0 Manager Priya earns Rs 100000.0 Priya leads a team: true Priya's monthly pay: Rs 100000.0
Several ideas meet here:
Managerdid not declaregetName(), yetm.getName()works: it was inherited.hasTeam()is new behaviour that only managers have.monthlyPay()is overridden:Managerprovides its own version with the same name and parameters. Inside it,super.monthlyPay()calls the parent's version, so the salary logic is not repeated.describe()lives inEmployeeand callsmonthlyPay(). For a manager, Java runs the manager'smonthlyPay, which is why Priya's description includes her allowance. This is polymorphism at work.- The fields are
protected, so subclasses can read them.
If the parent has no no-argument constructor, the child must call super(...) with arguments; otherwise the compiler reports error: constructor Employee in class Employee cannot be applied to given types. Java 25's flexible constructor bodies (see Constructors) also allow checks before super(...), so you can reject bad arguments before the parent constructor runs.
@Override
Always put @Override on a method that is meant to replace a parent method. It asks the compiler to check that a matching method really exists in a superclass. If you mistype the name, say tostring instead of toString, you get error: method does not override or implement a method from a supertype instead of a silent bug.
Polymorphism in action
A variable of a superclass type can hold an object of any subclass. When you call an overridden method through that variable, Java looks at the actual object at run time and calls its version. This is called dynamic dispatch.
public class Fleet {
public static void main(String[] args) {
Vehicle[] fleet = {
new Vehicle("HR12AB1234"),
new Car("RJ14CD5678", true),
new Truck("UP32EF9012", 8),
new AutoRickshaw("KL07GH3456")
};
int km = 20;
for (Vehicle v : fleet) {
System.out.printf("%-14s %-13s Rs %6.1f%n",
v.getClass().getSimpleName(), v.registration, v.fare(km));
}
}
}
class Vehicle {
final String registration;
Vehicle(String registration) {
this.registration = registration;
}
double fare(int km) {
return 10.0 * km;
}
}
class Car extends Vehicle {
private final boolean airConditioned;
Car(String registration, boolean airConditioned) {
super(registration);
this.airConditioned = airConditioned;
}
@Override
double fare(int km) {
double base = 14.0 * km;
return airConditioned ? base * 1.1 : base;
}
}
class Truck extends Vehicle {
private final int tonnes;
Truck(String registration, int tonnes) {
super(registration);
this.tonnes = tonnes;
}
@Override
double fare(int km) {
return 25.0 * km + 100.0 * tonnes;
}
}
class AutoRickshaw extends Vehicle {
AutoRickshaw(String registration) {
super(registration);
}
@Override
double fare(int km) {
return 30 + 12.0 * Math.max(0, km - 2);
}
}Vehicle HR12AB1234 Rs 200.0 Car RJ14CD5678 Rs 308.0 Truck UP32EF9012 Rs 1300.0 AutoRickshaw KL07GH3456 Rs 246.0
The loop knows only about Vehicle, yet each object calculates its fare its own way. Tomorrow you can add an ElectricBus class without changing the loop at all. This is the real benefit of polymorphism: code that works with the general type stays the same while new special types are added.
Casting and instanceof patterns
Putting a Dog into an Animal variable is an upcast, and it is automatic. Going back down, from Animal to Dog, is a downcast and needs care: the object might not be a dog at all. The instanceof operator tests the type, and since Java 16 it can also declare a variable of the tested type in one step. This is called a type pattern.
public class Casting {
static void inspect(Animal a) {
a.speak();
if (a instanceof Dog d) { // test and cast in one step
d.fetch();
} else if (a instanceof Cat c && c.lives > 5) {
System.out.println("A cat with " + c.lives + " lives left");
}
}
public static void main(String[] args) {
Animal pet = new Dog(); // upcast: automatic
inspect(pet);
inspect(new Cat());
Object o = new Cat();
System.out.println(o instanceof Animal);
System.out.println(o instanceof Dog);
}
}
class Animal {
void speak() { System.out.println("..."); }
}
class Dog extends Animal {
@Override
void speak() { System.out.println("Woof"); }
void fetch() { System.out.println("Fetching the ball"); }
}
class Cat extends Animal {
int lives = 9;
@Override
void speak() { System.out.println("Meow"); }
}Woof Fetching the ball Meow A cat with 9 lives left true false
Through an Animal variable you can only call methods that Animal declares; fetch() needs a Dog. If you force a wrong cast, such as (Dog) new Cat() stored in an Animal, the code compiles but fails at run time with java.lang.ClassCastException: class Cat cannot be cast to class Dog. The pattern form a instanceof Dog d avoids that, because d exists only when the test succeeds.
The Object class, equals and hashCode
Every class that does not extend anything else extends java.lang.Object. That is where toString, equals and hashCode come from. The inherited equals behaves like == (same object), so two coins with the same value and year are "not equal" until you override it. Whenever you override equals, also override hashCode, so that equal objects produce equal hash codes; collections such as HashSet and HashMap depend on this.
import java.util.Objects;
public class Eq {
public static void main(String[] args) {
Coin a = new Coin(10, 2019);
Coin b = new Coin(10, 2019);
System.out.println(a == b);
System.out.println(a.equals(b));
System.out.println(a.hashCode() == b.hashCode());
}
}
class Coin {
private final int value;
private final int year;
Coin(int value, int year) {
this.value = value;
this.year = year;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof Coin c)) return false;
return value == c.value && year == c.year;
}
@Override
public int hashCode() {
return Objects.hash(value, year);
}
}false true true
Writing these methods by hand is tedious. For classes that simply hold data, Java's records generate equals, hashCode and toString for you; see Records & Enums.
Rules and limits
- Java allows single inheritance of classes: a class extends exactly one parent. A class can implement many interfaces, as you will see in Abstract Classes & Interfaces.
- A
finalclass cannot be extended (Stringis final:class MyString extends Stringgiveserror: cannot inherit from final String). Afinalmethod cannot be overridden. - Overloading means same name, different parameters, chosen at compile time. Overriding means same name and parameters in a subclass, chosen at run time.
- An overriding method cannot be less visible than the one it replaces; a
publicmethod stayspublic.
Common mistakes
- Using inheritance just to reuse code. Inherit only when the child truly is a kind of parent. A
Caris aVehicle, but aCaris not anEngine; give it anEnginefield instead (this is called composition). - Forgetting
@Override. A typo creates a new method instead of overriding. - Downcasting without checking. Use
instanceofpatterns to avoidClassCastException. - Overriding
equalsbut nothashCode. Equal objects will behave strangely inHashSetandHashMap.
Key points
class Child extends Parentinherits the parent's fields and methods; Java classes have exactly one parent.- A subclass constructor calls
super(...)to set up the parent part. - Overriding replaces a parent method; mark it with
@Override, and usesuper.method()to reuse the parent's version. - Polymorphism: a parent-type variable can hold any subclass object, and the object's own method version runs.
x instanceof Type ttests a type and gives a ready-cast variable (Java 16+).- Every class extends
Object; overrideequalsandhashCodetogether.
Exercise
Create a class Ticket with a passenger name and a method price() returning 500. Create subclasses SleeperTicket (price 500 + 300) and TatkalTicket (the parent's price plus 30 percent, using super.price()), each with a toString showing the class name, passenger and price. Put one of each in a Ticket[] array with passengers Isha, Farhan and Meera, print them all, and print the total collected.
Show solution
public class Booking {
public static void main(String[] args) {
Ticket[] tickets = {
new Ticket("Isha"),
new SleeperTicket("Farhan"),
new TatkalTicket("Meera")
};
double total = 0;
for (Ticket t : tickets) {
System.out.println(t);
total += t.price();
}
System.out.println("Total collected: Rs " + total);
}
}
class Ticket {
protected final String passenger;
Ticket(String passenger) {
this.passenger = passenger;
}
double price() {
return 500;
}
@Override
public String toString() {
return getClass().getSimpleName() + " for " + passenger + ": Rs " + price();
}
}
class SleeperTicket extends Ticket {
SleeperTicket(String passenger) {
super(passenger);
}
@Override
double price() {
return 500 + 300;
}
}
class TatkalTicket extends Ticket {
TatkalTicket(String passenger) {
super(passenger);
}
@Override
double price() {
return super.price() * 1.3;
}
}Ticket for Isha: Rs 500.0 SleeperTicket for Farhan: Rs 800.0 TatkalTicket for Meera: Rs 650.0 Total collected: Rs 1950.0
toString is written once in Ticket, but getClass() and price() are resolved on the actual object, so each line shows the right class name and price.
Quick Quiz
1. What does super(name, salary) do in a subclass constructor?
super(...) runs the superclass constructor to set up the inherited part of the object.
2. Vehicle v = new Truck(...); v.fare(10); Which fare runs if Truck overrides it?
Dynamic dispatch picks the method of the actual object at run time.
3. What does if (a instanceof Dog d) give you when the test is true?
The type pattern tests the type and binds a ready-cast variable to the same object.
4. You override equals in a class. What else should you override?
Equal objects must have equal hash codes, or hash-based collections misbehave.
Learn Java with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
