What is the difference between compile-time and runtime polymorphism?
Compile-time (static) polymorphism is resolved by the compiler using the declared types, mainly through method overloading. Runtime (dynamic) polymorphism is resolved by the JVM using the actual object, through method overriding and dynamic dispatch. Fields, static methods and private methods are never resolved at runtime.
Explain it simply
Think of a railway announcement. Before the train leaves, the station prints a timetable that says "Platform 3: Train to Patna". That decision is fixed in advance from the information on paper. That is compile-time polymorphism: the compiler looks at the types written in your code and decides which method to call before the program ever runs.
Now think of the ticket checker who walks in and asks "who are you?" A student gets a concession check, a senior citizen a different check, a regular passenger a third. The checker decides only after seeing the actual person. That is runtime polymorphism: the JVM looks at the real object and picks its version of the method.
Example
class Printer {
String print(Object o) { return "Object"; }
String print(String s) { return "String"; }
}
class Animal {
String name = "animal";
String sound() { return "..."; }
static String kind() { return "Animal"; }
}
class Dog extends Animal {
String name = "dog";
@Override String sound() { return "Woof"; }
static String kind() { return "Dog"; }
}
public class Poly {
public static void main(String[] args) {
Object o = "hello";
System.out.println(new Printer().print(o)); // prints: Object (chosen from declared type)
Animal a = new Dog();
System.out.println(a.sound()); // prints: Woof (chosen from actual object)
System.out.println(a.name); // prints: animal (fields are not polymorphic)
System.out.println(a.kind()); // prints: Animal (static: picked from declared type)
}
}Even though o holds a String, the compiler only knows it as Object, so it picks print(Object). But a.sound() runs Dog's method, because overriding is decided by the real object.
How it works
Compile-time polymorphism
- Achieved with method overloading (and, in some languages, operator overloading; Java only overloads
+for strings internally and does not let you define your own). - The compiler looks at the method name and the declared types of the arguments and picks the best match. The choice is written into the bytecode.
Runtime polymorphism
- Achieved with method overriding, through inheritance or interfaces.
- The compiler checks that the method exists on the declared type, but the JVM chooses which implementation to run by looking at the actual class of the object. This is called dynamic dispatch or late binding.
- Only instance methods that can be overridden take part.
static,privateandfinalmethods, and all fields, are bound at compile time.
Real-world use
Runtime polymorphism is what makes plug-in designs work. A List<PaymentMethod> holding UPI, card and net banking objects can be processed with one loop calling pay(). A logging library calls Logger.log() without knowing whether the output goes to a file or the console. Compile-time polymorphism makes APIs friendly, like String.valueOf accepting an int, a double, a char or an Object.
Common mistakes
- Thinking overloads are chosen by the runtime type of the argument. Java uses single dispatch: only the object the method is called on is checked at runtime; arguments are matched at compile time.
- Expecting fields to behave like methods. Declaring a field with the same name in a child class creates a second, hidden field.
- Calling static method "overriding". It is method hiding.
- Saying runtime polymorphism is slow. Modern JVMs inline most of these calls when only one or two classes are actually seen at a call site.
What to say in the interview
"Compile-time polymorphism is resolved by the compiler, mainly through method overloading: it picks a method by looking at the name and the declared types of the arguments. Runtime polymorphism is resolved by the JVM through method overriding: a parent reference can point to a child object, and the child's version runs based on the actual object, which is called dynamic dispatch. Only overridable instance methods are dynamic; fields, static methods and private methods are bound at compile time. For example, Animal a = new Dog(); a.sound() runs Dog's sound, but a.name reads Animal's field."
Follow-up questions
- Why does
print(o)callprint(Object)whenoholds a String? - Can we achieve runtime polymorphism with data members?
- What is the difference between static binding and dynamic binding?
- What is double dispatch, and how does the Visitor pattern use it?
Go deeper
Read these once the basics above feel easy.
The compiler and JVM split the work. At compile time, the compiler picks a method signature from the declared type, say Animal.sound(), and writes an invokevirtual instruction for it (or invokeinterface for interface types). At runtime the JVM takes that signature and finds the most specific implementation in the actual object's class, walking up the hierarchy if needed. private methods, constructors and super.method() calls use invokespecial, and static methods use invokestatic, so none of these are dispatched on the object's runtime class.
HotSpot implements virtual calls with a per-class method table (a vtable), where an overriding method takes the same slot as the method it replaces, so dispatch is one indexed lookup. In practice the JIT does better: it profiles each call site, and if it only ever sees one receiver class (monomorphic) or two (bimorphic), it inlines the target with a quick type check, removing the call overhead completely. Sites that see many classes (megamorphic) fall back to the table lookup. Because Java dispatches only on the receiver, choosing behaviour based on two runtime types, such as which shape collides with which, needs double dispatch: the first object calls a method on the second and passes itself, so the second dispatch picks the overload for the first object's concrete type. The Visitor pattern is built on this idea.