You have written System.out.println(42) and System.out.println("hi") in the same program, and Java never once asked you to pick between printlnInt and printlnString. That single name accepting many argument types is not magic — it is method overloading, and it is one of the everyday conveniences that keeps Java readable. Without it, every type you ever wanted to print, log, or convert would need its own invented name, and your code would drown in near-identical helpers.
An overload is a second method that shares the first one's name but takes different parameters. This lesson is about predicting, with confidence, exactly which of several same-named methods the compiler will choose for a given call — because predicting wrong in your head is the difference between reading a stack trace and writing one.
flowchart LR S["signature = name + parameter types"] --> N["add(int, int)"] S --> N2["add(int, int, int)"] RT["return type is excluded"] -.-> S style S fill:#3776ab,color:#fff style RT fill:#b45309,color:#fff
What the compiler calls a signature
A method's signature is its name plus the ordered list of its parameter types — add(int, int) and add(int, int, int) are two different signatures, and add(int, double) is a third. The compiler uses exactly this to tell methods apart, which means two methods can share a name only when their parameter lists differ in type, order, or count.
Notice what the signature pointedly does not include: the return type. int add(int, int) and double add(int, int) have the same signature, so the compiler treats them as one method declared twice — a duplicate, not an overload. We will exploit that fact to read a common error in a moment.
Chosen at compile time, not runtime
Overload resolution is a compile-time decision. The compiler looks only at the declared types of your arguments, never at the values they happen to hold at runtime. Hand it a variable declared int and it routes to the int overload even if you know that, at runtime, the number could just as well have fit a double. There is no cleverness and no profiling — just types read straight off the source.
That is what makes overloading predictable. Once you can read the declared types at each call site, you can trace every route by hand, which is exactly the skill the exercises below sharpen.
public class Main {
static String describe(int n) {
return "int " + n;
}
static String describe(double n) {
return "double " + n;
}
static String describe(String s) {
return "text " + s;
}
public static void main(String[] args) {
System.out.println(describe(10));
System.out.println(describe(2.5));
System.out.println(describe("hi"));
}
}
flowchart TD
C["describe(arg) called"] --> Q{"exact type match available?"}
Q -->|"yes"| EXACT["exact overload chosen"]
Q -->|"no"| W{"widens to a parameter type?"}
W -->|"yes"| WUSE["widening overload chosen"]
W -->|"no"| B["boxing or varargs as last resort"]
style EXACT fill:#15803d,color:#fff
style WUSE fill:#3776ab,color:#fff
style B fill:#b45309,color:#fff
How the compiler chooses
When you call an overloaded name, the compiler does not guess. It walks a fixed ladder. First it hunts for an exact match — a parameter type identical to the argument's. Find one and the search ends. If no overload matches exactly, it tries widening: an int argument can satisfy a long or double parameter, because widening primitive conversions lose no information. Only if widening fails does it fall back to boxing (wrapping the primitive in its object type) and finally to varargs.
The widening chain runs byte → short → int → long → float → double, always toward a wider type. Memorise that order and you can predict widening at a glance: an int can fill a long slot, never the reverse.
public class Main {
static String describe(long n) {
return "long " + n;
}
public static void main(String[] args) {
System.out.println(describe(5)); // 5 is int; no describe(int) exists
System.out.println(describe(5L)); // 5L is long; exact match
}
}
flowchart TD
C["describe(5): both describe(long) and describe(double) apply"] --> Q{"is one overload more specific?"}
Q -->|"yes: long widens to double"| PICK["describe(long) chosen"]
Q -->|"no: neither dominates"| AMB["ambiguous, compile error"]
style PICK fill:#15803d,color:#fff
style AMB fill:#b91c1c,color:#fff
style Q fill:#3776ab,color:#fff
Most specific wins; ties are errors
Sometimes more than one overload is reachable by widening. Call describe(5) against describe(long) and describe(double) and both apply, because an int widens to either. The compiler does not give up — it picks the most specific one, meaning the overload whose parameter is closest to the argument in the widening chain. long sits before double, so long is more specific, and describe(long) wins.
When no overload is closer than another, the call is ambiguous and the build fails — the compiler refuses to guess on your behalf. Two methods greet(int, double) and greet(double, int) called as greet(5, 5) hit exactly this wall: each parameter favours a different overload, so neither dominates and the call is rejected.
public class Main {
static String describe(long n) {
return "long " + n;
}
static String describe(double n) {
return "double " + n;
}
public static void main(String[] args) {
System.out.println(describe(5));
}
}
Exact match. Three overloads share the name describe. Predict all three lines — each argument finds a different overload.
public class Main {
static String describe(int n) {
return "int " + n;
}
static String describe(double n) {
return "double " + n;
}
static String describe(String s) {
return "text " + s;
}
public static void main(String[] args) {
System.out.println(describe(10));
System.out.println(describe(2.5));
System.out.println(describe("hi"));
}
}
10is anintliteral — it matchesdescribe(int)exactly.2.5is adoubleliteral — it matchesdescribe(double)."hi"is aString— it matchesdescribe(String).
Widening. Only a long overload exists. Both calls print the same line — explain to yourself why the int argument is still accepted.
public class Main {
static String describe(long n) {
return "long " + n;
}
public static void main(String[] args) {
System.out.println(describe(5));
System.out.println(describe(5L));
}
}
5is anint, but there is nodescribe(int)— so the compiler widensinttolong.5Lis already along, an exact match.Both routes land on the same overload, so both print
long 5.
Most specific. describe(5) is reachable by BOTH overloads through widening. Predict the single line — which one does the compiler pick?
public class Main {
static String describe(long n) {
return "long " + n;
}
static String describe(double n) {
return "double " + n;
}
public static void main(String[] args) {
System.out.println(describe(5));
}
}
5is anint; it widens to bothlonganddouble.In the widening chain,
longcomes beforedouble, solongis more specific.The most specific applicable overload wins — so
describe(long)is chosen.
A class declares these two methods. Why does it fail to compile?
static int score(int n) { return n; }
static double score(int n) { return n; }
The signature is name plus parameter types; the return type is excluded. Both methods have the signature score(int), so the compiler sees one method declared twice and reports a duplicate. Overloads must differ in their parameter lists.
Given these two overloads, what happens when you call greet(5, 5)?
static void greet(int a, double b) { ... }
static void greet(double a, int b) { ... }
Both arguments are int. For greet(int, double) the first matches exactly and the second widens; for greet(double, int) it is the reverse. Neither overload dominates the other, so the compiler cannot pick one and reports the call as ambiguous. Fix it by making the argument types unambiguous, e.g. greet(5, 5.0).
Recap
- A method's signature is its name plus its parameter types — the return type is excluded.
- Overloading reuses one name across different parameter lists; the compiler selects by walking exact match → widening → boxing → varargs.
- Widening follows the chain
byte→short→int→long→float→double. - When several overloads apply, the most specific one wins; when none is more specific, the call is ambiguous and fails to compile.
- Resolution is a compile-time decision based on declared types only — no runtime values are consulted.
Next you will see where a method's variables live and die — scope, lifetime, and the surprises of shadowing.