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
NoSuchMethodErroror 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)