One reference type, many behaviours: the JVM picks the overriding method of the actual object at runtime.
Runtime polymorphism
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Payment> payments = List.of(new Card(100), new Upi(250), new Cash(40));
double total = 0;
for (Payment p : payments) { // one loop, three behaviours
System.out.println(p.describe());
total += p.fee();
}
System.out.println("total fees = " + total);
}
}
abstract class Payment {
protected final double amount;
Payment(double amount) { this.amount = amount; }
abstract double fee();
String describe() { return getClass().getSimpleName() + " " + amount + " fee " + fee(); }
}
class Card extends Payment { Card(double a) { super(a); } double fee() { return amount * 0.02; } }
class Upi extends Payment { Upi(double a) { super(a); } double fee() { return 0; } }
class Cash extends Payment { Cash(double a) { super(a); } double fee() { return 1; } }Card 100.0 fee 2.0
Upi 250.0 fee 0.0
Cash 40.0 fee 1.0
total fees = 3.0Static type vs runtime type
| Question | Decided by | When |
|---|---|---|
| Which methods may I call? | Reference (static) type | Compile time |
| Which overload is chosen? | Static types of the arguments | Compile time |
| Which override runs? | Runtime class of the object | Runtime (dynamic dispatch) |
| Which field is read? | Reference type — fields are not polymorphic | Compile time |
| Which static method runs? | Reference type | Compile time |
How dynamic dispatch works
Compile
javacchecks the method exists on the static type and emitsinvokevirtual Shape.area()(orinvokeinterface).Look up
At runtime the JVM reads the object's actual class and finds
areain its method table (vtable) — the most specific override.Optimise
The JIT often inlines the call when only one implementation has been seen (monomorphic call site), so polymorphism is usually free.
Upcasting and downcasting
public class Main {
public static void main(String[] args) {
Animal a = new Dog(); // upcast: implicit, always safe
a.speak();
// a.fetch(); // error: Animal has no fetch()
if (a instanceof Dog d) { // pattern matching (Java 16+): test + cast
d.fetch();
}
Dog d2 = (Dog) a; // explicit downcast: checked at runtime
d2.fetch();
Animal c = new Cat();
try {
Dog wrong = (Dog) c; // compiles, fails at runtime
wrong.fetch();
} catch (ClassCastException e) {
System.out.println("ClassCastException");
}
}
}
class Animal { void speak() { System.out.println("..."); } }
class Dog extends Animal {
@Override void speak() { System.out.println("Woof"); }
void fetch() { System.out.println("fetching"); }
}
class Cat extends Animal { @Override void speak() { System.out.println("Meow"); } }Woof
fetching
fetching
ClassCastExceptionOverloading is decided at compile time
public class Main {
static String greet(Object o) { return "object"; }
static String greet(String s) { return "string"; }
public static void main(String[] args) {
Object o = "hello"; // runtime type String, static type Object
System.out.println(greet(o)); // overload chosen from the STATIC type
System.out.println(greet("hello"));
System.out.println(o.toString().length()); // override chosen at runtime
}
}object
string
5What is dynamic method dispatch?
Are fields polymorphic?
Can you achieve polymorphism without inheritance?