Compile-Time Polymorphism
Compile-time polymorphism means the Java compiler decides which method to call while it is compiling your code — before a single line runs. Because everything is resolved early, it is also called static polymorphism or early binding.
What Is Polymorphism?
Polymorphism literally means “many forms.” In Java, one name can refer to multiple method implementations. There are two flavors:
| Type | Resolved by | Mechanism |
|---|---|---|
| Compile-time (static) | Compiler | Method Overloading |
| Runtime (dynamic) | JVM | Method Overriding |
This page focuses entirely on the compile-time variety.
Method Overloading — The Engine of Compile-Time Polymorphism
Method overloading means defining multiple methods with the same name inside the same class, each with a different parameter list. The compiler looks at the number, types, and order of arguments at the call site and picks the right version.
class Calculator {
// Version 1 — two ints
int add(int a, int b) {
return a + b;
}
// Version 2 — three ints
int add(int a, int b, int c) {
return a + b + c;
}
// Version 3 — two doubles
double add(double a, double b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(2, 3)); // calls version 1
System.out.println(calc.add(2, 3, 4)); // calls version 2
System.out.println(calc.add(2.5, 3.5)); // calls version 3
}
}
Output:
5
9
6.0
The compiler inspects each call, matches it to the best-fitting signature, and bakes the direct method reference into the bytecode. No decision-making happens at runtime.
Rules for Overloading
You must change the parameter list in at least one of these ways:
- Number of parameters —
add(int, int)vsadd(int, int, int) - Types of parameters —
add(int, int)vsadd(double, double) - Order of parameter types —
print(String, int)vsprint(int, String)
Warning: Changing only the return type is NOT valid overloading and will not compile. The compiler resolves the call before it knows which return type you expect.
class Demo {
int getValue() { return 10; }
// double getValue() { return 10.0; } // COMPILE ERROR — same signature
}
Overloading with Type Promotion
When no exact match exists, Java automatically promotes smaller numeric types to larger ones to find a match. The promotion chain is:
byte → short → int → long → float → double
class Promoter {
void show(long x) {
System.out.println("long: " + x);
}
}
public class PromoDemo {
public static void main(String[] args) {
Promoter p = new Promoter();
p.show(42); // int 42 is promoted to long — no exact int match
}
}
Output:
long: 42
Note: If multiple promotions are equally valid (e.g., both
show(long)andshow(float)exist), the compiler picks the most specific one —longin this case because it is a narrower promotion thanfloat.
Overloading with Autoboxing
Since Java 5, primitive types can be autoboxed to their wrapper counterparts during overload resolution. However, the compiler prefers an exact primitive match or type promotion before it resorts to autoboxing.
class BoxDemo {
void process(Integer x) { System.out.println("Integer wrapper: " + x); }
void process(long x) { System.out.println("long primitive: " + x); }
}
public class AutoboxTest {
public static void main(String[] args) {
BoxDemo bd = new BoxDemo();
bd.process(5); // widening (int→long) preferred over autoboxing (int→Integer)
}
}
Output:
long primitive: 5
Tip: The compiler’s priority order is: exact match → widening primitive promotion → autoboxing → varargs. Knowing this prevents subtle bugs.
Overloading with Varargs
Varargs methods (Type... args) can be overloaded too, but the compiler always tries a non-varargs match first.
class Printer {
void print(String s) { System.out.println("single: " + s); }
void print(String... strings) { System.out.println("varargs: " + strings.length + " items"); }
}
public class VarTest {
public static void main(String[] args) {
Printer p = new Printer();
p.print("hello"); // matches single-String version first
p.print("a", "b", "c"); // must use varargs version
}
}
Output:
single: hello
varargs: 3 items
Overloading in Constructors
Constructors can be overloaded just like methods — a common pattern for providing flexible object creation.
class Point {
double x, y, z;
Point() {
this(0, 0, 0);
}
Point(double x, double y) {
this(x, y, 0);
}
Point(double x, double y, double z) {
this.x = x; this.y = y; this.z = z;
}
@Override
public String toString() {
return "(" + x + ", " + y + ", " + z + ")";
}
}
public class PointDemo {
public static void main(String[] args) {
System.out.println(new Point());
System.out.println(new Point(1, 2));
System.out.println(new Point(1, 2, 3));
}
}
Output:
(0.0, 0.0, 0.0)
(1.0, 2.0, 0.0)
(1.0, 2.0, 3.0)
Notice the use of this(...) to chain constructors — this keeps logic in one place. Read more on the this Keyword and Constructors pages.
Under the Hood
When the Java compiler encounters an overloaded call like calc.add(2, 3), it performs overload resolution — a compile-time algorithm defined in the Java Language Specification (JLS §15.12):
- Phase 1: Find all applicable methods without autoboxing or varargs (widening only).
- Phase 2: Find all applicable methods with autoboxing/unboxing, still no varargs.
- Phase 3: Find all applicable methods allowing varargs.
The compiler picks the most specific applicable method from the winner set of each phase. If two methods are equally specific, you get an ambiguous-call compile error.
The selected method is recorded directly in the bytecode as an invokevirtual (or invokestatic for static methods) instruction with the fully resolved method descriptor — (II)I for add(int, int), (DI)I for add(double, int), etc. The JVM executes this instruction without any extra lookup at the call site; the heavy lifting is already done.
// javap -c output for calc.add(2, 3)
invokevirtual #4 // Method Calculator.add:(II)I
This is fundamentally different from runtime polymorphism, where the JVM must consult the vtable at the call site to dispatch to the right overriding implementation. Compile-time dispatch is cheaper — there is zero vtable lookup cost.
Tip: Use
javap -c MyClass.classto inspect the bytecode yourself and see which descriptor the compiler chose. The javap tool page walks you through it step by step.
Compile-Time vs Runtime Polymorphism — Quick Comparison
| Aspect | Compile-Time | Runtime |
|---|---|---|
| Resolved at | Compile time | Runtime (JVM) |
| Mechanism | Method overloading | Method overriding |
| Binding type | Static / early binding | Dynamic / late binding |
| Inheritance required? | No | Yes |
| Performance | Slightly faster (no vtable lookup) | Tiny overhead from vtable |
| Bytecode instruction | invokevirtual / invokestatic with fixed descriptor | invokevirtual with vtable dispatch |
Common Mistakes to Avoid
- Overloading by return type only — does not compile; the signature must differ in parameters.
- Ambiguous overloaded calls — passing
nullto overloaded methods that each accept a reference type causes an ambiguity error. Fix it with a cast:method((String) null). - Confusing overloading with overriding — overloading is same class, different parameters; overriding is subclass, same signature. See Overloading vs Overriding for the full comparison.
- Forgetting wrapper vs primitive preference — widening beats autoboxing, so
process(5)binds toprocess(long)beforeprocess(Integer).
Related Topics
- Method Overloading — the full reference for overloading rules and edge cases
- Runtime Polymorphism — how the JVM dispatches overridden methods dynamically
- Overloading vs Overriding — a side-by-side comparison of both concepts
- vtable & Dynamic Dispatch — the JVM mechanism behind runtime polymorphism
- Constructors — using constructor overloading to build flexible objects
- Polymorphism — the overview page covering both flavors of polymorphism