Method Overloading in Java: Widening, Boxing and Varargs
Method overloading in Java: methods share a name but differ in parameters. The compiler picks one at compile time, widening before boxing before varargs.
- Course: Java study plan
- Module: Methods
- Kind: Lesson
- Reading time: 14 min
- Runtime: OpenJDK 18
What is method overloading in Java?
Method overloading in Java means declaring several methods with the same name but different parameter lists, such as max(int, int) and max(double, double). The compiler picks one at compile time from the declared types of the arguments, preferring widening over boxing and boxing over varargs. Overloads cannot differ only in their return type.
Lesson
Two methods with the same name and different parameter lists are overloads. System.out.println has ten of them; Math.max has four; you will write your own to offer convenient variants of one operation. The compiler chooses among them at compile time by a three-phase rule that is precise, occasionally surprising, and a favourite interview topic.
What can be overloaded
static int max(int a, int b)
static long max(long a, long b)
static double max(double a, double b)
static int max(int a, int b, int c)
Overloads differ in the number or types of parameters (the signature). They may not differ only in return type — int f() and long f() in one class is an error, because a call f() could not choose. Parameter names are irrelevant. Overloads may have different return types, modifiers and exceptions, as long as the parameter lists differ.
Overloading is resolved statically: the compiler looks at the declared types of the argument expressions, not what they hold at run time. That is the crucial difference from overriding (Module 8), which is resolved at run time by the receiver's actual class.
The three phases
Given a call, the compiler finds the applicable overloads in three phases and stops at the first phase that finds any:
- Exact match or primitive widening / reference subtyping — no boxing, no varargs.
- With boxing/unboxing allowed.
- With varargs allowed.
Within a phase, the most specific method wins (an int parameter is more specific than long, String more specific than Object). If two are equally specific, the call is ambiguous and fails to compile.
static void f(long x) { System.out.println("long"); }
static void f(Integer x) { System.out.println("Integer"); }
static void f(Object x) { System.out.println("Object"); }
static void f(int... x) { System.out.println("varargs"); }
f(5); // "long" — phase 1: int widens to long; boxing (Integer) is phase 2
Widening beats boxing. Remove f(long) and the answer becomes "Integer" (phase 2, boxing, and Integer is more specific than Object). Remove that too and it is "Object" (still phase 2: box to Integer, then subtype to Object). Only with all three gone does varargs win.
Widening preferences
Among primitives, the compiler picks the smallest widening: with f(long), f(float) and f(double) available, f(5) calls f(long). char widens to int before anything else: f('a') with f(int) and f(long) calls f(int).
byte/short/char never narrow to match: f((short) 1) cannot call f(byte).
Reference types: most specific wins
static void g(Object o) { … "Object" }
static void g(String s) { … "String" }
g("hi"); // "String" — more specific
g(null); // "String" — null converts to any reference; String is more specific than Object
Object o = "hi";
g(o); // "Object" — the DECLARED type of o is Object; resolution is static
The last line is the one to remember: the argument's static type decides, not the object inside. And g(null) with g(String) and g(Integer) is ambiguous — neither is more specific than the other — and fails to compile; cast the null: g((String) null).
Boxing pitfalls
static void h(long x) { … }
static void h(Integer x){ … }
Integer boxed = 5;
h(boxed); // "Integer" — exact match in phase 1 (no conversion needed)
h(5); // "long" — widening (phase 1) beats boxing (phase 2)
A related classic: List<Integer> list; list.remove(1) calls remove(int index) — removing the element at index 1 — not remove(Object o), because the exact primitive match wins. To remove the value 1: list.remove(Integer.valueOf(1)).
Overloading and null
System.out.println(null) is ambiguous (char[] and String both apply). println((Object) null) compiles. Passing bare null to an overloaded method is a smell for this reason.
Overloading versus defaults
Java has no default parameter values. Overloads are the way to offer defaults: the short version calls the full one.
void connect(String host) { connect(host, 80, 30_000); }
void connect(String host, int port) { connect(host, port, 30_000); }
void connect(String host, int port, int timeoutMs) { … the real work … }
One implementation, several entry points. This "telescoping" pattern is standard; beyond three or four parameters, a builder (Module 7) reads better.
Overloads and constructors
Constructors overload the same way, and this(...) chains them (Module 7). The same resolution rules apply to new Foo(5).
Guidelines
- Overloads should do the same thing for different inputs.
add(int)andadd(String)doing unrelated work is a naming failure. - Avoid overloads whose parameters differ only by boxing or widening (
f(int)vsf(Integer),f(long)vsf(int)) — callers cannot predict which they get.Math.max's four are fine because they behave identically. - Avoid overloads that both accept
null-able reference types at the same position. - Never rely on varargs to disambiguate.
@Override-style safety does not exist for overloads: a typo in a parameter type silently creates a new overload instead of an error. Tests catch it; the compiler does not.
Interview angle
- "Can methods be overloaded by return type?" No.
- "
f(5)withf(long)andf(Integer)— which runs?"f(long): widening before boxing. - "Is overloading resolved at compile time or run time?" Compile time, from the arguments' declared types.
- "
list.remove(1)on aList<Integer>?" Removes index 1 — exact match onint. - "What does
g(null)do withg(String)andg(Object)?" Callsg(String), the more specific.
Key takeaways
- Overloads differ in parameter lists; return type alone does not count.
- Resolution is static (declared types) in three phases: widening/subtyping → boxing → varargs; most specific wins; ties are errors.
- Widening beats boxing beats varargs;
StringbeatsObject;nullpicks the most specific reference overload or is ambiguous. - Use overloads for defaults and same-behaviour variants; avoid boxing-only differences.
Common questions
Can we overload a method by changing only the return type in Java?
No. Overloads must differ in the number or types of their parameters; int f() and long f() in one class is a compile error, because a call f() gives the compiler no way to choose. Overloads may still have different return types when their parameter lists differ.
What is the difference between overloading and overriding in Java?
Overloading is several methods with the same name and different parameters, chosen at compile time from the arguments' declared types. Overriding is a subclass redefining an inherited method with the same signature, chosen at run time from the object's actual class.
Which overload does f(5) call when f(long) and f(Integer) both exist?
f(long). Overload resolution runs in three phases, first without boxing, then with boxing, then with varargs, and stops at the first phase that finds a match; widening int to long is allowed in phase one, while boxing to Integer needs phase two.
Why does list.remove(1) remove the element at index 1?
On a List<Integer>, remove(1) matches remove(int index) exactly, which beats remove(Object o) because that would need boxing. To remove the value 1 instead, call list.remove(Integer.valueOf(1)).
Does Java have default parameter values?
No. Java offers defaults through overloads that fill in the missing values and call the full version: connect(host) calls connect(host, 80, 30_000). Beyond three or four parameters, a builder usually reads better.
Exercises
Area overloads
Write three overloads of area: area(int side) for a square, area(int w, int h) for a rectangle, and area(double radius) for a circle (Math.PI * r * r). Read an integer n, then n lines of the form square 3, rect 3 4 or circle 2.5, call the matching overload, and print each area with two decimals.
Note that area(3) must call the int version and area(2.5) the double one — parse the arguments with the right types.
Example: square 3 → 9.00; circle 1 → 3.14.
Which overload?
Define four overloads named describe: describe(int), describe(long), describe(double) and describe(Object), each returning its own name ("int", "long", "double", "Object"). Read n tokens of the form <kind>:<value> where kind is int, long, double, char, short, Integer or String; declare a variable of exactly that type, call describe with it, and print <token> -> <chosen overload>. Widening beats boxing; a char or short widens to int; an Integer is an Object.
Example: 4 then int:5 char:a Integer:5 short:7 →
int:5 -> int
char:a -> int
Integer:5 -> Object
short:7 -> intIn this module: Methods
← Java is pass-by-value — always · Varargs — methods that take any number of arguments →