Java Method Overloading and Varargs Rules (1Z0-831)
Method overloading in Java means declaring several methods with the same name but different parameter type lists, and letting the compiler choose one for each call from the declared types of its arguments; a var-arg (variable-arity) method is one whose last parameter, written Type..., accepts any number of arguments as an array. Oracle 1Z0-831 objective 3.3 asks you to implement both, which in practice means predicting which overload a call binds to, whether it compiles at all, and what array a var-arg method receives. This lesson covers what makes a legal overload, the compiler's three resolution phases (widening, then boxing, then varargs), the most-specific rule and ambiguity, var-arg declaration and call rules, and why overloads are chosen at compile time while overrides are chosen at run time.
On this page10 sections
- What counts as a legal overload in Java
- How the compiler chooses an overload: three phases
- Phase 1: exact matches and primitive widening
- Widening beats boxing — including reference widening
- Phase 2: box then widen is allowed, widen then box is not
- Reference overloads, null and ambiguity
- Declaring var-arg methods: Type... is an array parameter
- What array a var-arg method actually receives
- Phase 3: varargs are the last resort, and when varargs overloads are ambiguous
- Overloads are bound at compile time, overrides at run time
- Decide whether two declarations are legal overloads, knowing that only the parameter types in order form the signature
- Trace overload resolution through its three phases: identity and widening, then boxing and unboxing, then variable arity
- Apply the primitive widening order and the no-narrowing rule for method arguments, including constants, char and short
- Explain why box-then-widen is allowed and widen-then-box is not, and pick the most specific reference overload, including for null
- Declare var-arg methods correctly and predict the array they receive for no arguments, an existing array, null and a single wrapped value
- Recognise ambiguous calls between var-arg overloads and between sibling reference types
- Distinguish compile-time overload selection from run-time overriding, including the equals(Point) trap
What counts as a legal overload in Java
Two methods in the same class (declared or inherited) are legal overloads when they share a name but differ in their parameter types, in order. That list of types is the method's signature; nothing else in the declaration counts. If two declarations end up with the same name and the same parameter types, the class does not compile, whatever else differs.
| Changed between the two declarations | Creates a new overload? |
|---|---|
| Number of parameters | Yes |
Type of a parameter, including int versus Integer or long | Yes |
Order of parameters of different types, e.g. (String, int) versus (int, String) | Yes |
| Return type only | No — compile error |
| Parameter names only | No — compile error |
static, access modifier, final or the throws clause only | No — compile error |
public class Main {
static double rate(long amount, char band) { return 1.5; }
static float rate(long cents, char tier) { return 2f; }
public static void main(String[] args) { }
}This fails with method rate(long,char) is already defined in class Main. The two methods differ in return type and parameter names, but both have the parameter list (long, char). Once the parameter lists differ, the return types, modifiers and throws clauses of overloads are free to differ too. Generic type parameters are erased for this check, so void f(List<String> a) and void f(List<Integer> a) clash as well (name clash: ... have the same erasure).
How the compiler chooses an overload: three phases
The compiler picks the overload at compile time, from the declared types of the arguments, by running up to three phases in order (JLS calls them strict invocation, loose invocation and variable-arity invocation) and stopping at the first phase that finds at least one applicable method — JLS 15.12.2. Within that phase it then chooses the most specific method; if no single method is most specific, the call is ambiguous and does not compile.
| Phase | Conversions allowed for each argument | Variable-arity methods? |
|---|---|---|
| 1 — strict invocation | Identity, primitive widening, reference widening (subclass to superclass or interface) | Only as plain array parameters |
| 2 — loose invocation | Everything in phase 1, plus boxing and unboxing (optionally followed by widening of the result) | Only as plain array parameters |
| 3 — variable arity | Everything in phase 2 | Yes, with loose arguments packed into an array |
The order explains the classic rules in one line: widening beats boxing, and boxing beats varargs. A later phase is never consulted once an earlier phase has a candidate, even if the later candidate looks like a closer fit. Declaration order in the class never matters.
One conversion is never available in any phase: narrowing. A method call will not convert double to float, long to int, or even the constant 10 to byte.
Phase 1: exact matches and primitive widening
In phase 1 an exact match wins; otherwise the compiler takes the overload whose parameter is the nearest widening target. The primitive widening order is byte → short → int → long → float → double, with char → int joining at int. When several overloads accept the argument by widening, the one whose parameter type itself widens to the others is the most specific: int beats long, long beats float, and float beats double.
public class Main {
static void w(int i) { System.out.print("int "); }
static void w(float f) { System.out.print("float "); }
static void w(double d) { System.out.print("double "); }
public static void main(String[] args) {
short s = 1;
char c = 'x';
w(s);
w(c);
w(7L);
w(2.5);
}
}This prints int int float double. A short and a char both widen to int, the nearest target. A long has no long overload here, and long → float is a legal widening (it may lose precision, but it is still widening), so w(7L) takes w(float), the more specific of the two floating-point overloads. The double literal 2.5 cannot narrow to float, so only w(double) fits.
One pair never converts in either direction: char and short. Both are 16 bits, but char is unsigned, so a char widens only to int, long, float and double, and a short (or byte) never widens to char. A char argument therefore skips a short overload and lands on the nearest of int, long, float or double, while a byte argument takes a short overload first.
No narrowing for arguments, not even for constants
The rule that lets short ok = 300; compile (a constant int that fits is narrowed in an assignment context) does not exist in an invocation context.
public class Main {
static void volume(short s) { System.out.println(s); }
public static void main(String[] args) {
short ok = 300;
volume(ok);
volume(300);
}
}volume(ok) is fine, but volume(300) is rejected; javac reports incompatible types: possible lossy conversion from int to short. If the class also declared volume(long) or volume(Object), the call would compile and go there instead (by widening, or by boxing 300 to Integer), even though the value fits in a short. The same applies to byte and char parameters, and a double literal such as 2.5 never reaches a float parameter.
Widening beats boxing — including reference widening
Because phase 1 runs before phase 2, an overload reachable by widening alone always beats one that needs boxing or unboxing. Phase 1 widening includes reference widening, so a wrapper argument reaches a Number or Object parameter in phase 1, before unboxing is even allowed.
public class Main {
static void tag(long x) { System.out.print("long " + x + " "); }
static void tag(Character x) { System.out.print("Character "); }
static void tag(Number x) { System.out.print("Number "); }
public static void main(String[] args) {
Short boxed = 3;
tag('B');
tag(boxed);
tag(boxed * 2);
}
}This prints long 66 Number long 6. tag('B'): a char widens to long in phase 1, so tag(Character), which needs boxing, is never considered — and the method sees the character's numeric value, 66, not the letter. tag(boxed): the Short widens to Number in phase 1, which beats unboxing to short and widening to long. tag(boxed * 2): arithmetic unboxes and promotes, so the expression's type is int, which widens to long.
The argument's static type is what matters: an expression like boxed * 2, a cast, or a ternary can change which overload is chosen even when the runtime value is the same.
Phase 2: box then widen is allowed, widen then box is not
In phase 2 an argument may be boxed (or unboxed) and the result may then be widened as a reference — but a primitive is never widened first and boxed afterwards. So int can become Integer and then Number or Object, but it can never become Long or Double, because that would need int → long → Long or int → double → Double.
| Argument | Parameter | Compiles? | Why |
|---|---|---|---|
3 (int) | Integer, Number, Object | Yes | Box to Integer, then reference-widen |
3 (int) | Double | No | Would need widen then box; Integer is not a Double |
3.0 or (double) 3 | Double | Yes | Box double to Double |
3f (float) | Double | No | Boxes only to Float |
Float.valueOf(3f) | Double | No | Unrelated wrapper classes never convert |
Float.valueOf(3f) | double | Yes | Unbox to float, then widen to double |
public class Main {
static void h(Double x) { System.out.print("Double "); }
static void h(Number x) { System.out.print("Number "); }
public static void main(String[] args) {
h(2);
h(2.0);
h(2f);
}
}This prints Number Double Number. h(2) boxes to Integer and h(2f) boxes to Float; only h(Number) accepts either. h(2.0) boxes to Double, which both overloads accept, and Double is the more specific. With h(Double) alone, h(2) would fail with int cannot be converted to Double. The same pattern holds for every wrapper: Long accepts 5L but not 5.
Reference overloads, null and ambiguity
Among reference-type overloads the compiler picks the one whose parameter type is a subtype of all the other applicable ones. A null argument is applicable to every reference parameter, so it goes to the most specific type — or fails to compile if two applicable types are unrelated.
public class Main {
static void report(Object o) {
System.out.print("Object ");
}
static void report(Exception e) {
System.out.print("Exception ");
}
static void report(RuntimeException e) {
System.out.print("RuntimeException ");
}
public static void main(String[] args) {
report(null);
report(new java.io.IOException());
report((Object) new IllegalStateException());
}
}This prints RuntimeException Exception Object. null picks report(RuntimeException) because RuntimeException is a subtype of both Exception and Object. An IOException is an Exception but not a RuntimeException. The cast (Object) changes the static type, so report(Object) runs although the object is a RuntimeException. No NullPointerException is possible from choosing an overload; nothing is dereferenced.
Now declare save(Integer) and save(String) and call save(null): the class fails with reference to save is ambiguous / both method save(Integer) in Main and method save(String) in Main match, because neither type is a subtype of the other. Adding a third overload that is a common supertype, such as save(Object), does not help — it is less specific than both, so it can never be the tie-breaker. A cast such as save((Integer) null) resolves it.
Declaring var-arg methods: Type... is an array parameter
A variable-arity (var-arg) parameter is written Type... name; a method may have only one, and it must be the last parameter. Inside the method it is an ordinary array of Type.
- Compiles:
int max(int first, int... rest),void join(char sep, Object... items),void grid(int[]... rows)(element typeint[], sorowsis anint[][]). - Does not compile:
max(int... rest, int first)andjoin(Object... a, char... b)(both reportvarargs parameter must be the last parameter— a second var-arg can never be last too), andgrid(int rows...)(the ellipsis belongs after the type, not the name).
Because the parameter's type is Type[], m(char... c) and m(char[] c) have the same signature and cannot both be declared:
public class Main {
static void fill(char[] c) { }
static void fill(char... c) { }
public static void main(String[] args) { }
}javac reports cannot declare both fill(char...) and fill(char[]) in Main, and no call in the class can rescue it. The two forms differ only at the call site, shown here for avg(long[] v) and mean(long... v):
| Call arguments | avg(long[] v) | mean(long... v) |
|---|---|---|
new long[] {4, 5} | Compiles | Compiles — the array stands for all the variable arguments |
4, 5 | Does not compile | Compiles — widened and packed into new long[] {4, 5} |
7 | Does not compile | Compiles — a one-element array |
| (none) | Does not compile | Compiles — receives new long[0] |
new int[] {1} | Does not compile | Does not compile — an int[] is not a long[], and it is not a long element either |
Mixing an array with loose values never works either: once there is more than one argument for the var-arg position, each must be convertible to the element type. Overriding follows the same signature rule: a subclass method m(int[] v) overrides m(int... v), because the signatures match — it compiles, though javac -Xlint warns [overrides] ... overriding method is missing '...'.
What array a var-arg method actually receives
The method receives a fresh array built from the loose arguments, or the caller's own array when a single argument is already assignable to the parameter's array type. The rules for the argument list decide which:
public class Main {
static int size(Object... o) {
return o == null ? -1 : o.length;
}
static void reset(String... s) { s[0] = "x"; }
public static void main(String[] args) {
Integer[] boxes = {1, 2};
long[] longs = {1, 2, 3, 4};
System.out.println(size() + " " + size(boxes)
+ " " + size(longs) + " " + size((Object) boxes)
+ " " + size(boxes, longs)
+ " " + size((Object[]) null));
String[] names = {"a", "b"};
reset(names);
reset(names[1], "c");
System.out.println(names[0] + " " + names[1]);
}
}The first line prints 0 2 1 1 2 -1 (javac warns about size(boxes) with "non-varargs call of varargs method with inexact argument type for last parameter", but compiles):
size()— no arguments: the compiler passesnew Object[0], nevernull, soo.lengthis safe.size(boxes)— anInteger[]is assignable toObject[](reference arrays are covariant), so it is passed as the array: length 2.size(longs)— along[]is not anObject[](primitive arrays are not covariant), so it becomes one element:new Object[] {longs}, length 1.size((Object) boxes)— the static type is nowObject, so it is wrapped as one element: length 1.size(boxes, longs)— two arguments, so both are packed as elements of a new array: length 2.size((Object[]) null)— an array-typednullis the array itself, sooisnull. An uncastnullas the only argument does the same, with a warning.- By contrast, a
nullcast to the element type, such as(Object) nullhere or(String) nullfor aString...parameter, is one element that happens to benull: length 1.
The second line prints x b. reset(names) passes the caller's array without copying, so the method's write changes names[0]; reset(names[1], "c") passes two values packed into a temporary array, so its write to that array's first slot is lost and names[1] stays "b".
Phase 3: varargs are the last resort, and when varargs overloads are ambiguous
A var-arg method is called in its variable-arity form only in phase 3, after every fixed-arity overload has failed by widening and by boxing. So any fixed-arity overload that can take the arguments wins over a var-arg one, however much converting it needs.
public class Main {
static void q(double x) { System.out.print("double "); }
static void q(Long x) { System.out.print("Long "); }
static void q(long... x) { System.out.print("long... "); }
public static void main(String[] args) {
q(4);
q(Long.valueOf(4));
q(4L);
q(4, 5);
q();
}
}This prints double Long double long... long.... q(4) widens to double in phase 1. q(Long.valueOf(4)) matches q(Long) exactly. q(4L) is the instructive one: a long reaches double by widening in phase 1, so q(Long), which needs boxing, and q(long...) are never reached, although both look like closer fits. Only the calls with two and with zero arguments, which no fixed-arity method can take, reach q(long...).
One subtlety: a single argument that is already of the parameter's array type (for example new long[] {1}) is applicable to long... in phase 1, as a plain long[] parameter. It therefore competes there with the other phase 1 candidates by the normal specificity rules, where an array type is more specific than Object, its supertype.
When two var-arg overloads both reach phase 3, the compiler compares their element types like ordinary parameters. With the call r('a', 'b'):
| Overloads declared | Result |
|---|---|
r(int...) and r(long...) | r(int...) — int is a subtype of long in the primitive subtyping order, and a char widens to both |
r(Object...) and r(Character...) | r(Character...) — Character is a subtype of Object |
r(char...) and r(Character...) | Ambiguous — a primitive and its wrapper are not subtypes of each other |
r(char, char...) and r(char...) | Ambiguous — neither is strictly more specific |
r(int, int) and r(char...) | r(int, int) — found by widening in phase 1, before varargs |
The ambiguous rows fail with reference to r is ambiguous. With r(int...) and r(long...) declared, r('a', 2L) prints long..., because the long argument cannot narrow to int, leaving only one applicable method.
Overloads are bound at compile time, overrides at run time
Which overload runs is fixed by the compiler from the declared type of the receiver and of each argument; only overriding (same signature in a subclass) is chosen at run time from the object's actual class. Mixing the two produces most of the surprising outputs in this objective.
public class Main {
static class Animal {
void speak(Number n) { System.out.print("Animal "); }
}
static class Dog extends Animal {
void speak(Integer i) { System.out.print("Dog "); }
}
public static void main(String[] args) {
Animal a = new Dog();
Dog d = new Dog();
d.speak(7.5);
a.speak(7);
d.speak(7);
}
}This prints Animal Animal Dog. d.speak(7.5) boxes to Double, which only the inherited speak(Number) accepts. For a.speak(7) the compiler searches Animal, the declared type of a, and finds only speak(Number). Dog.speak(Integer) is an overload, not an override (different parameter type), so at run time nothing replaces Animal's version, even though the object is a Dog. Through Dog d both methods are visible, and for 7 boxed to Integer, Integer is more specific than Number.
The same trap breaks equals:
import java.util.List;
public class Main {
static class Money {
final int cents;
Money(int cents) { this.cents = cents; }
public boolean equals(Money m) {
return m != null && m.cents == cents;
}
}
public static void main(String[] args) {
Money m = new Money(500);
Object o = new Money(500);
List<Money> wallet = List.of(new Money(500));
System.out.println(m.equals(new Money(500)) + " "
+ m.equals(o) + " " + wallet.indexOf(m));
}
}This prints true false -1. equals(Money) has overloaded Object.equals(Object), not overridden it. A call whose argument is declared as Money uses the new method, but m.equals(o) with Object o, and library code such as List.indexOf, List.contains or HashSet (which always call equals(Object)), bind to the inherited identity check. Declaring equals(Object o) with @Override lets the compiler catch the mistake. The overriding rules themselves belong to objective 3.5.
Tip. Objective 3.3 is examined through short, complete programs that declare two to four overloads and call them with literals, casts, wrapper objects, arrays or null. You are asked what the program prints, whether a declaration or call compiles, or which overloads can coexist. Expect to trace primitive widening chains, the no-narrowing rule for arguments, the order widening-boxing-varargs, the most-specific rule for reference types and its ambiguity errors, and the array a var-arg method receives. Some items combine overloading with inheritance, where the declared type of the receiver decides which methods the compiler can see, or with a mistaken equals overload. 'Compilation fails' is a real outcome, so check that every call has exactly one most specific applicable method before computing output.
- An overload must differ in its parameter types or their order; return type, parameter names, static, access modifiers and throws do not make a new signature.
- Overload resolution runs three phases in order — widening, then boxing/unboxing, then varargs — and stops at the first phase with a candidate.
- Method arguments are never narrowed: an int literal such as 300 does not reach a short parameter, 2.5 does not reach float, and char and short never convert into each other.
- Among widening candidates the nearest type wins: int before long before float before double, and long widens to float; a wrapper reaches Number or Object in phase 1, before unboxing.
- Box then widen is legal (int to Integer to Number); widen then box is not (int never becomes Long or Double).
- null goes to the most specific reference overload; two unrelated applicable types, such as Integer and String, make the call ambiguous, and adding Object does not break the tie.
- Type... must be the single, last parameter and is an array: it cannot coexist with a Type[] overload, receives an empty array for no arguments, and receives the caller's own array when one is passed.
- An int[] passed to Object... is wrapped as one element, while a String[] is passed as the array itself.
- Overloads are chosen from declared types at compile time; a subclass method with a different parameter type is an overload, not an override, so a superclass reference never sees it.
Frequently asked questions
Can Java methods be overloaded by return type alone?
No. A Java method's signature is its name plus its parameter types in order, so two methods that differ only in return type have the same signature and the class fails to compile with 'method ... is already defined'. Different return types are allowed only when the parameter lists also differ.
What is the order of overload resolution in Java?
The compiler tries three phases in order: first identity and widening conversions (primitive and reference), then the same plus boxing and unboxing, and finally variable-arity (varargs) invocation. It stops at the first phase that finds an applicable method and picks the most specific one in that phase, so widening beats boxing and boxing beats varargs.
Why does passing an int to a method that takes Long not compile?
Method invocation allows boxing followed by reference widening, but not primitive widening followed by boxing. An int boxes only to Integer, and Integer is not a subtype of Long, so a method taking Long accepts a long argument such as 5L or (long) 5 but not the int 5.
Can I pass an integer literal to a method that takes a byte?
No. The narrowing that lets byte b = 10; compile applies only to assignments of constants. Method arguments are never narrowed, so calling a method that takes byte with the literal 10 fails to compile unless you cast it, as in m((byte) 10), or another overload accepts an int.
Can a class declare both m(int... v) and m(int[] v)?
No. A varargs parameter Type... has the array type Type[], so both declarations have the signature m(int[]) and javac reports 'cannot declare both m(int...) and m(int[])'. The difference between the two forms is only at the call site: the varargs version also accepts loose values or no arguments.
What does a varargs parameter contain when the method is called with no arguments?
An empty array of the parameter's element type, never null. A varargs parameter is null only when the caller explicitly passes a null of the array type, such as m((String[]) null), or an uncast null, which compiles with a warning.
Why is a method call with null ambiguous in Java?
null can be passed to any reference parameter, so every reference-type overload is applicable. The compiler chooses the one whose parameter type is a subtype of all the others; if two applicable types are unrelated, such as Integer and String, neither is more specific and the call fails with 'reference to ... is ambiguous'. Casting the null to one type fixes it.
Is overload selection decided at compile time or at run time?
At compile time. The compiler picks the overload from the declared types of the receiver and the arguments; only overriding methods are dispatched at run time by the object's actual class. That is why a method such as equals(Money) does not override equals(Object): code that calls equals with an Object-typed argument, including collection methods such as contains and indexOf, still runs the inherited identity check.
Source
This lesson covers the "Applying Object-Oriented Principles in Java Programs" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.
- Java SE 25 Developer Professional (1Z0-831) exam page and topics — Oracle University
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.