Polymorphism & dynamic dispatch
Reference type vs object type — which method runs, which field you get.
Two types in play
Every object reference has two types: the declared reference type (what the compiler sees) and the runtime object type (what was actually constructed). Polymorphism arises from the gap between them.
class Shape {
String color = "gray";
String describe() { return "a shape"; }
}
class Circle extends Shape {
String color = "red"; // field hiding — not overriding
@Override
String describe() { return "a circle"; } // instance override
}
Shape s = new Circle(); // reference = Shape, object = Circle
System.out.println(s.describe()); // a circle — runtime object type
System.out.println(s.color); // gray — compile-time reference type
The golden rules:
| What | Resolution | Based on | |---|---|---| | Instance method call | Runtime dispatch | Object type | | Static method call | Compile-time | Reference type | | Field access | Compile-time | Reference type |
Polymorphism applies only to instance methods. Fields are never polymorphic — even if a subclass declares a field with the same name, accessing via a parent reference always gives the parent's field.
This design enables the Liskov Substitution Principle: any code accepting a Shape reference works correctly when passed a Circle, because the JVM always dispatches to the actual object's method.