By WebNest Studio Engineering Team
Java - Core Tutorial
Runtime Polymorphism and Dynamic Binding
Runtime polymorphism is the ability for a single method call, written once against a superclass or interface type, to execute different implementations depending on the actual (runtime) type of the object it is called on. It is made possible by method overriding combined with dynamic method dispatch — the mechanism the JVM uses to decide, at the moment of the call, exactly which overridden version of a method to run.
Polymorphism means "many forms", and Java has two kinds. Compile-time polymorphism is method overloading: the compiler picks a method from the arguments. Runtime polymorphism is method overriding: the JVM picks a method from the real object while the program runs. This lesson is about the second kind.
Why it matters
Dynamic binding is the mechanism behind polymorphism: the JVM decides which method body to run by looking at the real object, not at the variable's declared type. The catch is that this applies only to instance methods. Fields are bound at compile time, and code that reads a field through a parent reference gets the parent's value — a subtle bug that this lesson's exercise makes visible.
Watch: Runtime Polymorphism in Java – Complete Guide with Examples | Core Java Tutorial
How Java picks the method to call at runtime: overriding plus upcasting, dynamic method dispatch, early vs late binding, why fields and static, final and private methods are not polymorphic, downcasting with instanceof, polymorphism through interfaces and abstract classes, payment, shape and vehicle examples, and interview questions.
Why We Need Runtime Polymorphism
Imagine a checkout that accepts card, UPI and wallet payments. Without polymorphism, the checkout needs an if/else chain that tests which kind of payment it has, and every new payment type means editing that chain. With runtime polymorphism, the checkout calls payment.pay(amount) on a Payment reference and each subclass does its own thing. New payment types are new classes; the checkout code never changes. The same pattern drives drawing a list of Shapes, starting any Vehicle, or a framework calling the run() method of whatever task you give it.
The Three Ingredients: Inheritance, Overriding and Upcasting
Runtime polymorphism needs three things working together. There must be an inheritance (or interface) relationship, the child must override the method, and the call must go through a parent-type reference that holds a child object. That last step is called upcasting: Animal a = new Dog();. Upcasting is implicit and always safe, because every Dog is an Animal. The trade-off is that the compiler now only lets you call methods that Animal declares; a Dog-only method such as fetch() is out of reach through a.
Static Binding vs Dynamic Binding
Java resolves method calls in one of two ways. Static binding (also called early binding) happens at compile time and applies to private, static, and final methods, along with all fields — the compiler already knows exactly which method or field will be used, based purely on the declared (reference) type. Dynamic binding (late binding) happens at runtime and applies to overridden instance methods — the JVM looks at the actual object on the heap, not the reference type used to call it, and dispatches to that object's overridden version.
Virtual Method Invocation
When you call an instance method through a superclass reference variable that actually points to a subclass object, the JVM performs virtual method invocation: it inspects the real object's class at runtime and calls the most specific overriding version available in that object's actual class, walking up the inheritance chain only if no override exists at that level. This is why a Shape reference holding a Circle object calls Circle's area(), not Shape's.
Fields Are NOT Polymorphic
This is one of the most important and most frequently tested distinctions in Java: fields are resolved using static binding based on the reference type, not the object's actual type, even when a subclass declares a field with the same name (field hiding rather than overriding). Only instance methods participate in dynamic, runtime polymorphism — fields never do. Accessing obj.field through a superclass-typed reference always returns the superclass's field, regardless of what the object underneath actually is.
- Overridden instance methods: resolved by the object's actual runtime type (dynamic binding).
- Fields, static methods, and private/final methods: resolved by the reference's declared compile-time type (static binding).
- This asymmetry is why polymorphism is described as applying to "methods, not fields."
Why static, final and private Methods Are Not Polymorphic
Dynamic dispatch only has something to choose between when a method can be overridden. None of these three can, so the compiler binds them early.
- static methods belong to the class. A child's static method with the same signature hides the parent's, and a call through a parent reference runs the parent's version.
- final methods cannot be overridden, so there is only ever one version to call.
- private methods are not inherited; a same-named method in the child is a separate method that a parent reference cannot even see.
Downcasting and the instanceof Operator
To call a child-only method on an upcast reference, you cast back down: Dog d = (Dog) a;. Downcasting is explicit because it can fail. The compiler accepts any cast between related types, but if the object is not really a Dog, the JVM throws ClassCastException at runtime. Guard the cast with instanceof, which returns true only when the object is that type (or a subtype). Since Java 16 the check and the cast can be written together as if (a instanceof Dog d) { d.fetch(); }. If you find yourself downcasting often, it usually means the method belongs in the parent type as an overridable method.
Runtime Polymorphism with Interfaces and Abstract Classes
Runtime polymorphism does not need a concrete parent class. An abstract class can declare abstract double area(); with no body, which forces every concrete subclass (Circle, Rectangle) to override it. An interface such as Vehicle declares void start();, and each implementing class (Car, Bike) provides the body. In both cases you program against the general type — Shape s, Vehicle v — and the JVM dispatches to the real object's implementation. This is the style most real Java code and frameworks use.
Compile-Time vs Runtime Polymorphism
- Achieved by: compile-time uses method overloading; runtime uses method overriding.
- Decided by: compile-time uses the declared types of the arguments; runtime uses the actual class of the object.
- Binding: compile-time is static (early) binding; runtime is dynamic (late) binding.
- Inheritance needed: not for overloading; yes (a class or an interface) for overriding.
- Flexibility: overloading offers convenient variants of one operation; overriding lets new types plug in without changing calling code.
Examples
Overridden methods are polymorphic; fields are not
class Shape {
String label = "Shape";
String describe() {
return "A generic shape";
}
}
class Square extends Shape {
String label = "Square"; // hides Shape.label, does not override it
@Override
String describe() {
return "A square";
}
}
public class PolymorphismDemo {
public static void main(String[] args) {
Shape ref = new Square(); // superclass reference, subclass object
System.out.println(ref.describe()); // dynamic binding -> Square's version
System.out.println(ref.label); // static binding -> Shape's field
}
}
A square
Shape
An abstract class: every shape overrides area()
public class ShapeDemo {
public static void main(String[] args) {
Shape[] shapes = { new Circle(1), new Rectangle(2, 3) };
for (Shape s : shapes) {
System.out.printf("%s area = %.2f%n", s.name(), s.area());
}
}
}
abstract class Shape {
abstract double area(); // no body: every subclass must override it
String name() {
return getClass().getSimpleName();
}
}
class Circle extends Shape {
private final double r;
Circle(double r) { this.r = r; }
@Override
double area() { return Math.PI * r * r; }
}
class Rectangle extends Shape {
private final double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
@Override
double area() { return w * h; }
}
Circle area = 3.14
Rectangle area = 6.00
An interface: one method works for every Vehicle
public class VehicleDemo {
static void startTrip(Vehicle v) { // works for every Vehicle, now and later
v.start();
}
public static void main(String[] args) {
startTrip(new Car());
startTrip(new Bike());
}
}
interface Vehicle {
void start();
}
class Car implements Vehicle {
@Override
public void start() { System.out.println("Car: turn key, engine starts"); }
}
class Bike implements Vehicle {
@Override
public void start() { System.out.println("Bike: kick start"); }
}
Car: turn key, engine starts
Bike: kick start
Upcasting, safe downcasting with instanceof, and a ClassCastException
public class CastDemo {
public static void main(String[] args) {
Animal a = new Dog(); // upcasting: implicit and always safe
a.sound(); // Dog's override runs
// a.fetch(); // compile error: Animal has no fetch()
if (a instanceof Dog d) { // check, then downcast (Java 16+ pattern)
d.fetch();
}
Animal c = new Cat();
Dog wrong = (Dog) c; // compiles, but fails at runtime
}
}
class Animal {
void sound() { System.out.println("Some sound"); }
}
class Dog extends Animal {
@Override
void sound() { System.out.println("Woof"); }
void fetch() { System.out.println("Dog fetches the ball"); }
}
class Cat extends Animal {
@Override
void sound() { System.out.println("Meow"); }
}
Woof
Dog fetches the ball
Exception in thread "main" java.lang.ClassCastException: class Cat cannot be cast to class Dog (Cat and Dog are in unnamed module of loader 'app')
at CastDemo.main(CastDemo.java:12)
Common Mistakes
- Expecting ref.label to print the subclass value when ref is declared as the superclass type — fields are resolved by the reference type, not the object type.
- Assuming static methods participate in runtime polymorphism when called through an object reference — static methods are resolved by the reference's declared type at compile time.
- Believing polymorphism requires an explicit interface — it works equally well through plain class inheritance with overriding.
- Forgetting that private and final methods use static binding and therefore cannot be polymorphic, since they cannot be overridden.
- Calling a child-only method through a parent reference. The compiler checks the reference type, so
a.fetch()fails to compile even whenaholds aDog. - Downcasting without an
instanceofcheck, which compiles but throwsClassCastExceptionwhen the object is a different subtype.
Exercise
This program prints parent and then child method: the method call is polymorphic, but the field access is not. Add an overridable getName() method to both classes and use it in main so the first line prints child.
Expected output: child then child method
class Parent {
String name = "parent";
String who() {
return "parent method";
}
}
class Child extends Parent {
String name = "child";
@Override
String who() {
return "child method";
}
}
public class BindingDemo {
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.name);
System.out.println(p.who());
}
}
Show hints
p.nameis resolved from the declared typeParent, so it reads the parent's field.- A method is resolved from the object, so
p.getName()runs the child's version if it overrides it.
Show solution
class Parent {
String name = "parent";
String getName() {
return name;
}
String who() {
return "parent method";
}
}
class Child extends Parent {
String name = "child";
@Override
String getName() {
return name;
}
@Override
String who() {
return "child method";
}
}
public class BindingDemo {
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.getName()); // child
System.out.println(p.who()); // child method
}
}
Quick quiz
1. Parent and Child both declare a field name. What does Parent p = new Child(); p.name read?
- A. The Parent field
- B. The Child field
- C. Both, joined together
- D. It does not compile
Show answer
A. Field access is bound at compile time from the declared type of the reference, which is Parent.
2. Which kinds of method use static (compile-time) binding?
- A. Overridden instance methods
- B. static, private and final methods
- C. Abstract methods
- D. All methods
Show answer
B. These cannot be overridden, so the compiler knows exactly which method will run and binds the call early.
3. Does assigning a Child object to a Parent variable need an explicit cast?
- A. Yes, always
- B. Only for interfaces
- C. Only for abstract classes
- D. No, upcasting is implicit
Show answer
D. Every Child is a Parent, so the conversion is always safe and the compiler performs it automatically.
Interview questions
What is the difference between static binding and dynamic binding?
Show answer
Static binding happens at compile time: the compiler decides which method or field a name refers to, using the declared types. It applies to static, private and final methods, to overloaded method selection, and to all fields. Dynamic binding happens at runtime: for an overridable instance method, the JVM looks at the actual class of the object and runs that class's version.
What is upcasting and why is it useful?
Show answer
Upcasting is treating an object as one of its supertypes, for example storing a Square in a Shape variable. It is always safe and needs no cast. It is useful because code written against the general type works with every subtype: one method that accepts a Shape handles squares, circles and any shape added later.
What is dynamic method dispatch?
Show answer
It is the mechanism Java uses to implement runtime polymorphism. When an overridden method is called through a parent-type reference, the compiler only checks that the method exists in the parent type. At runtime the JVM looks at the actual class of the object and runs that class's version, walking up the hierarchy only if the class does not override it.
Animal a = new Dog(); can you call a Dog-only method on a?
Show answer
Not directly. The compiler checks method calls against the declared type Animal, so a.fetch() does not compile. You must downcast first, ideally with a check: if (a instanceof Dog d) d.fetch();. A cast without the check compiles but throws ClassCastException if a actually holds some other animal.
Can runtime polymorphism be achieved with data members?
Show answer
No. Fields are not overridden, only hidden, and a field access is bound at compile time from the reference type. Parent p = new Child(); p.name always reads Parent's field. To get polymorphic behaviour for data, expose it through an overridable getter method.
Key Points to Remember
- Runtime polymorphism works through overriding plus dynamic method dispatch, based on the object's actual runtime type.
- It needs inheritance (or an interface), an overridden method, and a parent-type reference to a child object (upcasting).
- The reference type decides which methods you may call; the object type decides which body runs.
- Static binding (fields, static/private/final methods) is resolved at compile time using the reference's declared type.
- Only overridden instance methods are polymorphic — fields are always resolved statically, even when hidden by a subclass.
- Upcasting is implicit and safe; downcasting is explicit and should be guarded with
instanceof. - Abstract classes and interfaces are the usual way to write polymorphic code in real projects.
Go deeper
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering