Ask ten freshers to define polymorphism and eight will recite "many forms" and stop there. That answer scores zero in an interview. What earns marks is explaining when the binding happens — at compile time for overloading, at runtime for overriding — and proving it with output you can predict.
Polymorphism builds directly on inheritance, so make sure you are comfortable with extends and overriding before diving in. Here we split the concept into its two forms, walk through dynamic dispatch line by line, and cover the trap questions.
The one-sentence version worth memorizing
Polymorphism lets the same method name trigger different behavior. Java delivers this in two ways:
| Form | Mechanism | Who decides | When |
|---|---|---|---|
| Compile-time (static) | Method overloading | Compiler | While compiling |
| Runtime (dynamic) | Method overriding | JVM | While running |
Everything else in this article hangs off that table.
Compile-time polymorphism: method overloading
Overloading means several methods in the same class share a name but differ in their parameter lists — different count, different types, or different order of types. The compiler looks at the arguments in your call and binds the matching method before the program ever runs.
public class PaymentService {
void pay(double amount) {
System.out.println("Paying " + amount + " via default method");
}
void pay(double amount, String upiId) {
System.out.println("Paying " + amount + " via UPI: " + upiId);
}
void pay(double amount, long cardNumber, int cvv) {
System.out.println("Paying " + amount + " via card");
}
public static void main(String[] args) {
PaymentService ps = new PaymentService();
ps.pay(499.0);
ps.pay(499.0, "student@upi");
ps.pay(499.0, 4111111111111111L, 123);
}
}
What to notice: three methods, one name, zero ambiguity — each call's argument list points to exactly one method. Changing only the return type is not overloading and will not compile, because the return type plays no part in the compiler's selection.
Common mistake: A common mistake beginners make is expecting
pay(5)to be ambiguous betweenpay(double)and an imagined widening chain. Java resolves overloads in a fixed order: exact match, then widening (int → long → float → double), then autoboxing, then varargs.pay(5)widensinttodoubleand compiles fine.
Runtime polymorphism: method overriding
Overriding happens across classes: a subclass redefines a method it inherited, keeping the same signature. The magic appears when a parent reference holds a child object.
class Notification {
void send(String msg) {
System.out.println("Generic notification: " + msg);
}
}
class EmailNotification extends Notification {
@Override
void send(String msg) {
System.out.println("Sending EMAIL: " + msg);
}
}
class SmsNotification extends Notification {
@Override
void send(String msg) {
System.out.println("Sending SMS: " + msg);
}
}
public class Dispatcher {
public static void main(String[] args) {
Notification[] queue = {
new EmailNotification(),
new SmsNotification(),
new Notification()
};
for (Notification n : queue) {
n.send("Your class starts at 7 PM");
}
// Output:
// Sending EMAIL: Your class starts at 7 PM
// Sending SMS: Your class starts at 7 PM
// Generic notification: Your class starts at 7 PM
}
}
What to notice: the loop variable n is always typed Notification, yet each iteration runs a different send(). The compiler only verifies that Notification has a send(String) method; the JVM picks the actual body at runtime by checking the real object's class. That selection process is dynamic method dispatch.
This is not an academic trick. Every framework you will use professionally depends on it — Spring calls your controller through interface references, JDBC calls whichever driver's implementation is on the classpath. The caller codes against the parent type; the runtime supplies the child behavior.
Upcasting, downcasting, and reference vs object type
The line Notification n = new EmailNotification(); is an upcast — child object, parent reference. It is always safe and needs no cast operator. Rules that follow from it:
- You can call only methods declared in the reference type.
n.someEmailOnlyMethod()will not compile even though the object has it. - Overridden methods run the object type's version (dynamic dispatch).
- To reach child-only members, downcast — and guard it with
instanceof:
if (n instanceof EmailNotification email) { // pattern matching, Java 16+
email.attachInvoice();
}
Pro tip: If you find yourself writing long
instanceofchains, that is a design smell. Push the varying behavior into an overridden method so dispatch does the branching for you — that is literally what polymorphism is for.
Overloading vs overriding: the comparison interviewers want
| Dimension | Overloading | Overriding |
|---|---|---|
| Where | Same class (or inherited into it) | Parent–child classes |
| Signature | Must differ in parameters | Must be identical |
| Return type | Can be anything | Same or covariant subtype |
| Access modifier | Anything | Cannot be narrower than parent's |
| Binding | Compile time | Runtime |
static methods |
Can be overloaded | Cannot be overridden (only hidden) |
private methods |
Can be overloaded | Not visible, so not overridable |
| Exceptions | No restriction | Cannot throw broader checked exceptions |
The traps: fields and static methods do not dispatch
Only instance methods are polymorphic. Fields and static methods resolve by reference type at compile time.
class Parent {
String label = "parent-field";
static String tag() { return "parent-static"; }
}
class Child extends Parent {
String label = "child-field";
static String tag() { return "child-static"; }
}
public class TrapDemo {
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.label); // parent-field
System.out.println(p.tag()); // parent-static
}
}
What to notice: both lines print the parent version despite the object being a Child. Field access and static calls are bound to the declared type — this exact snippet appears in written tests more often than any other polymorphism question.
Interview note: When asked "predict the output," first check whether the member is an instance method. If it is not, ignore the object type entirely and answer from the reference type. This single rule solves most trick questions on our Java OOP interview questions page.
Where polymorphism meets abstraction
Runtime polymorphism becomes truly powerful when the parent type is abstract — an abstract class or interface that cannot be instantiated and exists purely as a contract. List<String> list = new ArrayList<>(); is polymorphism through an interface, and it is the reason you can swap ArrayList for LinkedList in one line. Read Abstraction in Java next to complete that half of the picture, and see how it plays out at scale in the Java Collections Framework.
Practice checklist
Before moving on, you should be able to do these without notes:
- Write one class with three overloads of the same method and predict which each call binds to.
- Write a parent-reference/child-object example and explain the output using the words "dynamic method dispatch."
- State the two members that never dispatch dynamically (fields, static methods).
- Explain why
@Overrideon a hiding static method is a compile error.
If you can, you are ahead of most candidates. To drill this with mentors and mock interviews rather than alone, this is core material in our Java Full Stack course.
Frequently Asked Questions
What are the two types of polymorphism in Java?
What is dynamic method dispatch?
Is method overloading really polymorphism?
Can we override static methods in Java?
Why can't we overload methods by changing only the return type?
Does polymorphism apply to fields (variables) in Java?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

