1️⃣ Static Type vs. Actual (Runtime) Type
In Java, every variable has:
- Static type (compile-time type) --- what the compiler sees and uses to check method calls.
- Actual type (runtime type) --- the real class of the object stored in that variable.
Example:
Human man = new Man();
-
Human
→ static type\ -
Man
→ actual (runtime) type
Static type is fixed at compile time. Actual type is only known at
runtime.
2️⃣ Overloading Is Based on Static Type
Take this code:
public class StaticDispatch {
static abstract class Human {}
static class Man extends Human {}
static class Woman extends Human {}
public void sayHello(Human people) {
System.out.println("hello,people");
}
public void sayHello(Man man) {
System.out.println("hello,man");
}
public void sayHello(Woman woman) {
System.out.println("hello,woman");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
Output:
hello,people
hello,people
Why?
Because overloading resolution happens at compile time --- the
compiler only sees that the variables man
and woman
are declared as
Human
. It doesn't care that at runtime the actual objects are Man
and Woman
.
If you want the compiler to choose the more specific overload, you must
cast:
sr.sayHello((Man) man);
sr.sayHello((Woman) woman);
Now the static types at the call site are Man
and Woman
, so the
specific overloads will be chosen.
3️⃣ Overload Resolution Priority
Sometimes, multiple overloaded methods can match. Java applies a
specific priority order to decide which one to use.
Let's look at a classic example:
public class A {
public static void sayHi(Object arg) { System.out.println("object"); }
public static void sayHi(int arg) { System.out.println("int"); }
public static void sayHi(long arg) { System.out.println("long"); }
public static void sayHi(Character arg) { System.out.println("Character"); }
public static void sayHi(char arg) { System.out.println("char"); }
public static void sayHi(char... arg) { System.out.println("char..."); }
public static void sayHi(Serializable arg) { System.out.println("Serializable"); }
public static void main(String[] args) {
sayHi('a');
}
}
Run with all methods
Output → char
Because 'a'
is literally a char
.
Comment out sayHi(char)
Output → int
Reason: 'a'
can be promoted to its numeric value 97.
Comment out sayHi(int)
Output → long
Reason: widening: char -> int -> long
.
Comment out sayHi(long)
Output → Character
Reason: autoboxing 'a'
to Character
.
Comment out sayHi(Character)
Output → Serializable
Reason: Character
implements Serializable
.
Comment out sayHi(Serializable)
Output → Object
Reason: autobox to Character
then upcast to Object
.
Comment out sayHi(Object)
Output → char...
Reason: fallback to varargs.
👉 Priority summary:
Primitive exact match >
Widening primitive conversion >
Autoboxing to wrapper >
Autoboxing to implemented interfaces >
Autoboxing to Object >
Varargs
4️⃣ Why It Matters
This might seem academic, but understanding overloading resolution
and the compile-time vs runtime type distinction helps you:
- Avoid surprising method calls.
- Write cleaner APIs (be careful with overloads that can cause ambiguity).
- Debug confusing
NoSuchMethodError
or ambiguous method errors.
For interview prep or deep JVM knowledge, this topic often comes up ---
especially the static vs runtime type concept and how overload
resolution works.
⚡️ Key Takeaways
- Overloading uses compile-time types. The JVM doesn't do dynamic dispatch for overloaded methods.
- Casting affects overload resolution (because it changes the static type at the call site).
- Autoboxing & varargs are fallback mechanisms with well-defined priority.
- Be mindful with overloaded APIs --- too many similar signatures can cause unexpected calls.
💬 Have you encountered unexpected overload resolution in real projects
or interviews? Share your experiences below!
Top comments (0)