Runtime polymorphism is one of the most powerful features of Java's Object-Oriented Programming model. It allows a parent class reference or interface reference to refer to different child objects, while the actual object at runtime determines which overridden method implementation executes.
Unlike compile-time polymorphism, where the compiler selects an overloaded method based on the arguments, runtime polymorphism involves method overriding and dynamic method dispatch.
Core idea: With runtime polymorphism, the reference may be general, but the behavior comes from the actual object.
Why Do We Need Runtime Polymorphism?
Imagine an application that supports several types of notifications: email, SMS, and push notifications. Every notification needs a send() operation, but each type sends the notification differently.
Instead of writing separate code that checks the notification type with multiple if-else statements, we can define a common parent type and allow each child class to provide its own implementation.
class Notification { void send() { System.out.println("Sending notification"); } } class EmailNotification extends Notification { @Override void send() { System.out.println("Sending email notification"); } } class SmsNotification extends Notification { @Override void send() { System.out.println("Sending SMS notification"); } }
Now the application can work with the general Notification type while allowing each object to perform its own specialized behavior.
The Basic Runtime Polymorphism Pattern
The most important pattern to recognize is:
Parent reference = new Child();
For example:
Notification notification = new EmailNotification();
notification.send();
The reference type is Notification, but the actual object created is an EmailNotification. Because send() is overridden, Java executes the implementation belonging to the actual object.
Remember: In Parent ref = new Child(), the parent type controls what the reference can access, while the child object's overridden method controls the runtime behavior.
Complete Example
class Animal { void sound() { System.out.println("Animal makes a sound"); } } class Dog extends Animal { @Override void sound() { System.out.println("Dog barks"); } } class Cat extends Animal { @Override void sound() { System.out.println("Cat meows"); } } public class Main { public static void main(String[] args) { Animal animal1 = new Dog(); Animal animal2 = new Cat(); animal1.sound(); animal2.sound(); } }
The output is:
Dog barks Cat meows
Notice something important: both variables are declared as Animal, and both calls use exactly the same method name, sound(). Yet different implementations execute.
That is runtime polymorphism in action.
Reference Type vs Object Type
To understand runtime polymorphism properly, you must distinguish between two types associated with a polymorphic reference.
| Type | Example | What It Determines |
|---|---|---|
| Reference type | Animal | Members accessible through the reference at compile time |
| Object type | Dog | Actual object and overridden instance-method behavior at runtime |
Consider:
Animal animal = new Dog();
The compiler sees an Animal reference. At runtime, the JVM is dealing with a Dog object.
This distinction explains many apparently confusing Java behaviors and becomes especially important when you learn upcasting and downcasting.
Runtime Polymorphism Through a Method Parameter
Runtime polymorphism becomes even more useful when a parent type is used as a method parameter.
class Payment { void pay() { System.out.println("Processing payment"); } } class CardPayment extends Payment { @Override void pay() { System.out.println("Processing card payment"); } } class UpiPayment extends Payment { @Override void pay() { System.out.println("Processing UPI payment"); } } public class Main { static void processPayment(Payment payment) { payment.pay(); } public static void main(String[] args) { processPayment(new CardPayment()); processPayment(new UpiPayment()); } }
The processPayment() method does not need to know the exact payment implementation. It accepts the common parent type Payment.
When a CardPayment object is supplied, its pay() implementation executes. When a UpiPayment object is supplied, its implementation executes.
Industry insight: This pattern is extremely common in real applications. Business logic can depend on an abstraction while different implementations provide specialized behavior.
Runtime Polymorphism with Collections
A collection can hold multiple child objects through a common parent type. This is one of the clearest practical demonstrations of runtime polymorphism.
Animal[] animals = {
new Dog(),
new Cat(),
new Dog()
};
for (Animal animal : animals) {
animal.sound();
}
The loop knows only that every element is an Animal. It does not need to check whether the object is a Dog or Cat. The JVM dispatches the correct overridden method for each object.
Runtime Polymorphism with Interfaces
Runtime polymorphism is not limited to class inheritance. Interfaces are one of the most important ways Java applications use polymorphism.
interface Payment { void pay(); } class CreditCardPayment implements Payment { @Override public void pay() { System.out.println("Paid using credit card"); } } class UpiPayment implements Payment { @Override public void pay() { System.out.println("Paid using UPI"); } } public class Main { public static void main(String[] args) { Payment payment = new CreditCardPayment(); payment.pay(); payment = new UpiPayment(); payment.pay(); } }
Here, the reference type is the interface Payment. The object can be any class that implements that interface. This is a major foundation for loosely coupled Java applications.
Why This Reduces Coupling
Suppose a service method directly creates a specific payment class:
CreditCardPayment payment = new CreditCardPayment();
payment.pay();
The code is now tightly connected to CreditCardPayment. If the application later needs another payment method, more code may need to change.
With polymorphism, the service can depend on the abstraction:
Payment payment = new CreditCardPayment();
payment.pay();
The surrounding code cares about what the object can do rather than being tightly coupled to its concrete implementation.
Runtime Method Selection
When an overridden instance method is called through a polymorphic reference, Java uses the runtime object to determine the implementation.
class Shape { void draw() { System.out.println("Drawing shape"); } } class Circle extends Shape { @Override void draw() { System.out.println("Drawing circle"); } } Shape shape = new Circle(); shape.draw();
The compiler validates that draw() is available through the Shape reference. At runtime, Java identifies the actual object as a Circle and executes the overridden Circle.draw() method.
Learning checkpoint: If the child overrides a parent instance method and a parent reference points to the child object, calling that method normally executes the child's overridden implementation.
What Runtime Polymorphism Does Not Mean
Runtime polymorphism does not mean that every member of the child class suddenly becomes available through the parent reference.
class Animal { void sound() { System.out.println("Animal sound"); } } class Dog extends Animal { @Override void sound() { System.out.println("Bark"); } void fetch() { System.out.println("Dog fetches the ball"); } } Animal animal = new Dog(); animal.sound(); // animal.fetch(); // Compile-time error
The actual object is a Dog, but the reference type is Animal. Since fetch() is not declared in Animal, it cannot be accessed through that reference without an appropriate cast.
Methods and Runtime Polymorphism
It is important to be precise when discussing runtime polymorphism. The classic runtime-dispatch behavior applies to overridden instance methods.
Static methods are associated with classes rather than being dynamically dispatched like overridden instance methods. Private methods are not inherited in the normal overriding sense, and final methods cannot be overridden.
Interview tip: If asked whether static methods participate in runtime polymorphism, do not simply say "yes." Static methods are hidden rather than overridden and are resolved based on the reference/class context.
Runtime Polymorphism vs Compile-Time Polymorphism
| Feature | Compile-Time Polymorphism | Runtime Polymorphism |
|---|---|---|
| Common technique | Method overloading | Method overriding |
| Binding | Early/static binding | Late/dynamic binding |
| Decision point | Compilation | Runtime |
| Based primarily on | Method arguments and signatures | Actual runtime object |
| Typical pattern | Same class, multiple parameter lists | Parent/interface reference to child implementation |
Common Beginner Mistakes
- Thinking the reference type always determines the implementation that executes.
- Confusing method overriding with method overloading.
- Assuming a parent reference can directly access child-specific methods.
- Forgetting that runtime polymorphism requires an appropriate inheritance or interface relationship.
- Assuming static methods are dynamically dispatched like overridden instance methods.
- Using type checks everywhere instead of allowing polymorphic behavior to do the work.
Best Practices
- Program against suitable abstractions such as parent classes or interfaces.
- Keep overridden methods focused on the specialized behavior of each child class.
- Use the @Override annotation when overriding a method to let the compiler catch signature mistakes.
- Prefer polymorphic design over large conditional structures when object-specific behavior naturally belongs to different classes.
- Use interfaces when you want unrelated classes to follow the same behavioral contract.
Interview Insights
A common interview question is: "What is runtime polymorphism in Java?"
A strong answer is: Runtime polymorphism is the ability to call an overridden instance method through a parent class or interface reference, where the method implementation is selected according to the actual object at runtime.
Interviewers may then ask you to explain Animal animal = new Dog();. The key points are that Animal is the reference type, Dog is the runtime object type, and an overridden instance method call can execute the Dog implementation.
Final Takeaway
Runtime polymorphism allows Java applications to work with general types while preserving the specialized behavior of concrete objects. It is primarily achieved through method overriding and dynamic method dispatch. The most important pattern to remember is Parent reference = new Child();. Once this relationship becomes clear, concepts such as method overriding, dynamic method dispatch, upcasting, and downcasting become much easier to understand.
