Polymorphism in Java
One name, many forms: compile-time polymorphism with overloading, run-time polymorphism with overriding, upcasting and dynamic method dispatch.
Learning objectives
- βExplain compile-time (overloading) and run-time (overriding) polymorphism
- βStore a subclass object in a parent-type reference (upcasting)
- βPredict which overridden method runs under dynamic method dispatch
- βDowncast safely with instanceof
π‘ Key points
- Polymorphism means many forms: the same method call can behave differently depending on the object.
- Compile-time polymorphism is method overloading. The compiler picks the version from the argument types before the program runs.
- Run-time polymorphism is method overriding. The JVM picks the version from the actual object while the program runs.
- Upcasting means a parent reference points to a child object, as in Animal a = new Dog(). It is automatic and always safe.
- Dynamic method dispatch: after Animal a = new Dog(), a.sound() runs Dog's sound(), because the object really is a Dog.
- The reference type decides which methods you are allowed to call. The object type decides which version actually runs.
- Downcasting (Dog d = (Dog) a;) needs an explicit cast. Check with instanceof first to avoid a ClassCastException.
π» Code examples(5)
class Printer {
void print(int n) {
System.out.println("int: " + n);
}
void print(double d) {
System.out.println("double: " + d);
}
void print(String s) {
System.out.println("String: " + s);
}
}
public class OverloadDemo {
public static void main(String[] args) {
Printer p = new Printer();
p.print(10);
p.print(9.5);
p.print("Amit");
p.print('A'); // no print(char) exists
}
}
int: 10 double: 9.5 String: Amit int: 65
class Animal {
void sound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Woof");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Meow");
}
}
public class Dispatch {
public static void main(String[] args) {
Animal a; // parent-type reference
a = new Dog(); // upcasting
a.sound();
a = new Cat();
a.sound();
a = new Animal();
a.sound();
}
}
Woof Meow Some sound
class Shape {
String name;
Shape(String name) { this.name = name; }
double area() { return 0; }
}
class Rectangle extends Shape {
double l, w;
Rectangle(double l, double w) {
super("Rectangle");
this.l = l;
this.w = w;
}
@Override
double area() { return l * w; }
}
class Triangle extends Shape {
double b, h;
Triangle(double b, double h) {
super("Triangle");
this.b = b;
this.h = h;
}
@Override
double area() { return 0.5 * b * h; }
}
public class ShapeDemo {
public static void main(String[] args) {
Shape[] shapes = {
new Rectangle(4, 5),
new Triangle(6, 4),
new Rectangle(3, 3)
};
double total = 0;
for (Shape s : shapes) {
System.out.println(s.name + ": " + s.area());
total += s.area();
}
System.out.println("Total area: " + total);
}
}
Rectangle: 20.0 Triangle: 12.0 Rectangle: 9.0 Total area: 41.0
class Animal {
void eat() {
System.out.println("Eating");
}
}
class Dog extends Animal {
void fetch() {
System.out.println("Fetching the ball");
}
}
public class CastDemo {
public static void main(String[] args) {
Animal a = new Dog(); // upcast
a.eat();
// a.fetch(); // error: Animal has no fetch()
if (a instanceof Dog) {
Dog d = (Dog) a; // downcast
d.fetch();
}
Animal plain = new Animal();
System.out.println(plain instanceof Dog);
}
}
Eating Fetching the ball false
class Customer {
String name;
Customer(String name) { this.name = name; }
double discount() { return 0.0; }
}
class PrimeCustomer extends Customer {
PrimeCustomer(String name) { super(name); }
@Override
double discount() { return 0.10; }
}
public class Billing {
static void printBill(Customer c, double amount) {
double pay = amount - amount * c.discount();
System.out.println(c.name + " pays Rs." + pay);
}
public static void main(String[] args) {
printBill(new Customer("Amit"), 2000);
printBill(new PrimeCustomer("Priya"), 2000);
}
}
Amit pays Rs.2000.0 Priya pays Rs.1800.0
π― Practice
Q1. Is method overloading compile-time or run-time polymorphism?+
Compile-time. The compiler chooses the overloaded version from the argument types. Overriding is run-time polymorphism, where the JVM chooses based on the actual object.
Q2. Animal a = new Cat(); and Cat overrides sound(). Which sound() runs on a.sound()?+
Cat's sound(). The object is a Cat, and overridden methods are chosen by the object's type (dynamic method dispatch), not by the reference type.
Q3. Why does Animal a = new Dog(); a.fetch(); fail to compile when only Dog has fetch()?+
The compiler checks every call against the reference type. Animal has no fetch(), so it reports 'cannot find symbol'. Downcast first: ((Dog) a).fetch();
Q4. What happens here? Animal a = new Animal(); Dog d = (Dog) a;+
It compiles, but it throws ClassCastException when it runs, because the object is not a Dog. Guard the cast with if (a instanceof Dog).
Q5. Give two differences between overloading and overriding.+
Overloading uses the same name with different parameters, usually in the same class, and is decided at compile time. Overriding uses the same name and the same parameters in a subclass, and is decided at run time.
π Notes
Overloading vs overriding
| Overloading | Overriding | |
|---|---|---|
| Where | Usually in one class | Parent and child class |
| Parameters | Must differ | Must be the same |
| Decided | At compile time, from argument types | At run time, from the object |
| Also called | Compile-time polymorphism | Run-time polymorphism |
| Annotation | None | @Override (optional, recommended) |
| Access rule | None | Cannot be more restricted than the parent's |
What is not polymorphic
Only overridable instance methods use dynamic dispatch. static, private and final methods are fixed at compile time, and so are fields:
class A { String tag = "A"; }
class B extends A { String tag = "B"; }
A obj = new B();
System.out.println(obj.tag); // A, not B
Fields follow the reference type, not the object. This is one more reason to keep fields private and reach them through methods, which are overridden.
A shortcut you may see
From Java 16, instanceof can test and cast in one step:
if (a instanceof Dog d) {
d.fetch();
}
The plain instanceof with a cast, as in example 4, works in every Java version.
Common mistakes
- Calling a child-only method through a parent reference. Upcasting hides those methods until you downcast.
- Casting without checking. That leads to ClassCastException at run time.
- Lowering access when overriding. Overriding a
publicmethod withoutpublicgives f() in Q cannot override f() in P (attempting to assign weaker access privileges). - Thinking you have overridden when you have overloaded.
void sound(String s)inDogis a new, overloaded method, not an override ofsound().@Overridecatches this.
Next: Day 16, Abstract Classes and Interfaces. You will write classes that define what must be done and leave how to subclasses.