Method Overloading
Method overloading lets a class declare several methods with the same name but different parameter lists, and the compiler — not the JVM at runtime — decides which one you meant.
Method overloading is Java's way of letting the same method name mean different things depending on what you pass it. println(int), println(String), and println(Object) are all overloads of println living side by side in PrintStream. The compiler picks the exact method to call at compile time, based purely on the declared types of the arguments — this is why overloading is sometimes called static (or compile-time) polymorphism, in contrast to overriding, which is resolved at runtime.
What Overloading Is
Two or more methods in the same class (or an accessible subclass) share a name but differ in their parameter list — the number, order, or types of parameters. The return type alone is not enough to distinguish overloads.
Four methods named add, distinguished by parameter count and types — this is a valid overload set.
A method's signature (for overload purposes) is its name plus its parameter types, in order. Parameter names and the return type are not part of the signature.
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
String add(String a, String b) { return a + b; }
}Valid and Invalid Overloads
For two methods to legally overload each other, their parameter lists must differ. Changing only the return type, only a parameter's name, or only an access modifier does not create a valid overload — the compiler rejects it as a duplicate method.
The compiler must be able to tell overloads apart purely from the argument types at a call site; return type is irrelevant to that decision.
// Valid — different parameter counts/types
void report(int code) { }
void report(int code, String message) { }
void report(String message) { }
// INVALID — differs only by return type; won't compile
// int compute(int x) { return x; }
// double compute(int x) { return x; } // compile error: duplicate methodHow the Compiler Resolves an Overload Call
When you call an overloaded method, the compiler searches for the best match in a fixed, three-phase order, and it stops at the first phase that yields exactly one applicable method:
This ordering means an exact or widening-primitive match always wins over a boxed match, and a boxed match always wins over a varargs match, if all three exist.
int -> long is a widening primitive conversion (Phase 1), which beats both boxing to Integer (Phase 2) and varargs (Phase 3).
Phase 1: Exact match, or match via widening primitive conversion
(int -> long, float -> double, etc.) and reference widening,
WITHOUT boxing/unboxing and WITHOUT varargs.
Phase 2: Same as above, but boxing/unboxing IS now allowed
(int <-> Integer), still no varargs.
Phase 3: Varargs methods are considered.class Pick {
static void show(long x) { System.out.println("long: " + x); }
static void show(Integer x) { System.out.println("Integer: " + x); }
static void show(int... x) { System.out.println("varargs"); }
public static void main(String[] args) {
show(5); // prints "long: 5"
// int widens to long in Phase 1 without needing boxing or varargs,
// so it wins over the Integer overload (needs boxing, Phase 2)
// and over the varargs overload (Phase 3).
}
}Widening, Boxing, and Overload Preference
Primitive widening (byte -> short -> int -> long -> float -> double, plus char -> int) is preferred over autoboxing when both are candidates, because widening is checked in Phase 1 and boxing only in Phase 2.
If you remove the long overload entirely, f(n) would then resolve to f(Integer) via autoboxing in Phase 2 — the compiler only reaches for boxing when no Phase 1 candidate exists.
class Widen {
static void f(long l) { System.out.println("long"); }
static void f(Integer i) { System.out.println("Integer"); }
public static void main(String[] args) {
int n = 10;
f(n); // prints "long" — widening beats boxing
}
}Overload Ambiguity
If more than one method is an equally good match within the same resolution phase, and neither is strictly more specific than the other, the compiler reports an ambiguous method call error rather than guessing.
Ambiguity is a compile-time error, caught before the program ever runs — it never manifests as unpredictable runtime behavior.
Mixing null literals with overloads that take different reference types is another classic ambiguity trap: null matches any reference-type parameter equally well unless one type is a strict subtype of the other.
class Ambiguous {
static void f(int a, long b) { }
static void f(long a, int b) { }
public static void main(String[] args) {
// f(1, 2); // compile error: reference to f is ambiguous
// Both overloads require exactly one widening conversion;
// neither is more specific than the other.
f(1, 2L); // fine — matches f(int, long) exactly
f(1L, 2); // fine — matches f(long, int) exactly
}
}class NullAmbiguity {
static void g(String s) { }
static void g(StringBuilder sb) { }
public static void main(String[] args) {
// g(null); // ambiguous — String and StringBuilder are unrelated
g((String) null); // fine, disambiguated with a cast
}
}Overloading With Varargs
Varargs methods are only considered in the final resolution phase, so a fixed-arity overload is always preferred when it applies exactly. This lets you add a varargs "catch-all" without disturbing more specific existing overloads.
class Logger {
static void log(String msg) { System.out.println("[1 arg] " + msg); }
static void log(String msg, Object arg) { System.out.println("[2 args] " + msg + " " + arg); }
static void log(String msg, Object... args) { System.out.println("[varargs] " + args.length + " extra"); }
public static void main(String[] args) {
log("hello"); // [1 arg]
log("hi", 42); // [2 args] — exact fixed-arity match beats varargs
log("many", 1, 2, 3); // [varargs] 3 extra — only varargs applies
}
}Overloading vs. Overriding
These are frequently confused but resolved at completely different times by completely different rules.
A method that has the same name and parameter list as one in its superclass overrides it; a method with the same name but a different parameter list overloads it — even if that happens across a subclass boundary.
Overloading Overriding
------------------------------------ ------------------------------------
Same name, DIFFERENT parameter list Same name, SAME parameter list
Resolved at COMPILE time Resolved at RUNTIME (dynamic dispatch)
Can be in the same class Requires a subclass relationship
Return type can differ freely Return type must be same or covariant
Chosen by declared argument types Chosen by the object's actual runtime typePractical Usage
Overloading is idiomatic for offering convenience entry points to the same conceptual operation — accepting a String, a File, or an InputStream for "read this data", or letting a format method accept different numbers of substitution arguments. Keep every overload doing the same conceptual thing; if the behaviors diverge meaningfully, separate method names communicate intent far better than overloading.
class Rectangle {
private final double width, height;
Rectangle(double side) { // square convenience overload
this(side, side);
}
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
double area() { return width * height; }
}Edge Cases
A few situations regularly trip people up in interviews and code review:
1. Passing a subclass instance to overloads taking a supertype vs. the exact subtype resolves to the overload matching the argument's declared (compile-time) type, not its runtime type — because overload resolution happens at compile time.
This is the opposite of overriding, where the actual runtime type would decide the behavior.
2. Autoboxing a null primitive wrapper and passing it where both a primitive overload and a wrapper overload exist can throw a NullPointerException during unboxing if the primitive overload is chosen.
class Animal { }
class Dog extends Animal { }
class Speak {
static void say(Animal a) { System.out.println("Animal"); }
static void say(Dog d) { System.out.println("Dog"); }
public static void main(String[] args) {
Animal a = new Dog();
say(a); // prints "Animal" — resolved by the DECLARED type Animal,
// even though the object is actually a Dog at runtime.
}
}Best Practices
Use overloading for genuinely related operations with the same essential meaning. Avoid overloads that differ only in a way easy to pass by accident (e.g., f(int, long) vs f(long, int)). Prefer varargs or a builder-style API over many near-identical overloads for optional parameters. Document briefly if one overload simply delegates to another, since readers benefit from seeing the canonical implementation.
Complete Example
describe(10L) has no int, double, String, or long overload to match exactly or via primitive widening, so it eventually boxes to Long and matches Object — illustrating how far the resolution search can travel.
public class OverloadDemo {
static void describe(int n) {
System.out.println("int: " + n);
}
static void describe(double d) {
System.out.println("double: " + d);
}
static void describe(String s) {
System.out.println("String: " + s);
}
static void describe(Object o) {
System.out.println("Object: " + o);
}
public static void main(String[] args) {
describe(10); // int: 10
describe(3.14); // double: 3.14
describe("hi"); // String: hi
describe(10L); // Object: 10 (long widens to Object via boxing+widening, no exact/long overload here)
}
}Mental Model
Think of overload resolution as the compiler asking, at each call site, "given the declared types of these arguments, which single method signature fits best, checking exact matches first, then widening, then boxing, then varargs?" Once it answers that question, the choice is baked into the compiled bytecode — nothing about the objects' actual runtime types can change which overload gets called.
Quick Reference
Overload requirement: same name, different parameter list
NOT sufficient: different return type alone
Resolution order: 1) exact/widening 2) boxing/unboxing 3) varargs
Resolution time: compile time (uses declared/static types)
Ambiguous call: compile-time error, never a runtime guessKey Takeaways
Overloading means same method name, different parameter lists — return type alone never distinguishes overloads.
The compiler resolves overloads in three ordered phases: exact/widening match, then boxing/unboxing, then varargs.
Overload resolution is purely compile-time and uses the declared (static) type of arguments, unlike overriding which is runtime and uses the actual object type.
Ambiguous calls are caught at compile time, not resolved arbitrarily at runtime.
Prefer overloading only for genuinely related operations; reach for distinct method names when behavior diverges.
int compute(int x) { return x; } double compute(int x) { return x * 1.0; } int computeInt(int x) { return x; } double computeDouble(int x) { return x * 1.0; }
Two methods cannot overload on return type alone — this fails to compile as a duplicate method; give them distinct names or genuinely distinct parameter lists instead.
static void f(int a, long b) { } static void f(long a, int b) { } // f(1, 2); // ambiguous!
static void f(int a, long b) { } static void f(long a, int b) { } f(1, 2L); // explicit — resolves to f(int, long)
When two overloads are equally plausible for the same call, disambiguate with an explicit cast or literal suffix rather than relying on the compiler to guess — it won't; it will refuse to compile.
Animal a = new Dog(); say(a); // expecting Dog-specific overload to run say((Dog) a); // explicitly cast if you want the Dog overload chosen
Overload resolution uses the declared/static type, not the runtime type — assuming it behaves like polymorphic dispatch (overriding) is a common source of surprise.
Why can't two methods overload each other by differing only in return type?
Because overload resolution at a call site is based only on the argument types supplied, not on what the caller does with the result — the compiler would have no way to disambiguate int compute(int) from double compute(int) at a call like compute(5).
Given overloads f(long) and f(Integer), which one is chosen for f(5) where 5 is an int literal, and why?
f(long) is chosen, because int -> long is a widening primitive conversion considered in resolution Phase 1, while int -> Integer requires boxing, considered only in Phase 2; Phase 1 candidates always win when present.
Is overload resolution performed at compile time or runtime? What does this imply for code using a variable's declared type vs. its actual object type?
It is performed entirely at compile time, using the declared (static) type of each argument expression. This means passing a Dog stored in an Animal-typed variable to overloaded methods resolves against the Animal overload, even though the object is actually a Dog.
When does the compiler consider varargs overloads during resolution?
Only in the third and final phase, after both a no-boxing/widening-only phase and a boxing-allowed phase have failed to find exactly one applicable method — fixed-arity overloads are always preferred over varargs when they apply.
Why might calling an overloaded method with a null argument cause an ambiguous method call error?
null can match any reference type parameter equally well; if two overloads take unrelated reference types (e.g., String and StringBuilder) and neither is a subtype of the other, the compiler cannot determine which is more specific, so it reports ambiguity unless you cast the null.
What is the difference between overloading and overriding in terms of when each is resolved?
Overloading is resolved at compile time based on the declared parameter types at the call site (static/early binding); overriding is resolved at runtime based on the actual class of the object the method is invoked on (dynamic dispatch/late binding).
Write two overloads of a method printValue that would be genuinely ambiguous if called with printValue(1, 2), and explain why.
static void printValue(int a, long b) {} and static void printValue(long a, int b) {} — calling printValue(1, 2) requires exactly one widening conversion for either overload and neither is more specific than the other, so the compiler reports the call as ambiguous.
Write an overloaded set of methods named maxOf that returns the maximum of two ints, two doubles, and three ints, then call each from main.
public class MaxDemo { static int maxOf(int a, int b) { return a > b ? a: b; } static double maxOf(double a, double b) { return a > b ? a: b; } static int maxOf(int a, int b, int c) { return maxOf(maxOf(a, b), c); } public static void main(String[] args) { System.out.println(maxOf(3, 7)); // 7 System.out.println(maxOf(2.5, 1.1)); // 2.5 System.out.println(maxOf(4, 9, 6)); // 9 } } maxOf(3, 7) matches the two-int overload -> 7; maxOf(2.5, 1.1) matches the two-double overload -> 2.5; maxOf(4, 9, 6) matches the three-int overload, which internally reuses the two-int overload -> 9.
What does this program print, and why do the two calls resolve differently even though both arguments hold the same String value at runtime?
It prints "Object" then "String". show(o) resolves against the declared type of o, which is Object, regardless of the fact the object stored is actually a String at runtime. show("hello") uses a String literal whose declared type is String, so it matches the more specific String overload directly. This demonstrates that overload resolution uses static (compile-time) types, not runtime types.
Which overload of f is called for f(5), and why doesn't it match Integer, long, or Object instead?
It calls f(int) and prints "int". Resolution Phase 1 looks for an exact match or a widening-only match without boxing; 5 is exactly an int, so f(int) is an exact match and wins immediately — long (a widening match) and Integer/Object (boxing matches, Phase 2) are never even considered once an exact Phase 1 match exists.
class Test {
static void show(Object o) { System.out.println("Object"); }
static void show(String s) { System.out.println("String"); }
public static void main(String[] args) {
Object o = "hello";
show(o);
show("hello");
}
}class T {
static void f(int x) { System.out.println("int"); }
static void f(Integer x) { System.out.println("Integer"); }
static void f(long x) { System.out.println("long"); }
static void f(Object x) { System.out.println("Object"); }
public static void main(String[] args) {
f(5);
}
}