JAVA / METHODS
Varargs for methods that take any number of inputs
Write Java methods that accept any number of arguments with Type..., and predict exactly what array the compiler builds at each call site.
What you will learn
- Declare Type... last and treat it inside the method as a plain Type[]
- Call the same method with zero arguments, many arguments, or an existing array
- Predict overload winners: fixed-arity beats varargs before wrapping ever happens
- Spot the Object... shape trap: String[] spreads, int[] arrives as one element
Understanding Varargs for methods that take any number of inputs
Writing int... numbers declares a single parameter whose type is int[]. The three dots do not create a new kind of parameter; the compiled descriptor is the same one you would get from int[], with a flag recording that callers may use the shorthand. What the dots buy you is permission for the compiler to collect the loose arguments at the end of a call and allocate a fresh array from them before the jump. That single mechanism explains the syntax rules: because the array is built from whatever is left over at the tail of the argument list, the varargs parameter must come last, and a method can have only one.
Inside the body you have an array and nothing more, so numbers.length is the count the caller actually wrote and a for-each loop walks it. A call with no arguments hands you a length-zero array rather than null, which is why for (int n : numbers) on sum() simply runs zero times instead of throwing. And because the parameter type really is int[], you may pass an array you already hold: the compiler sees a type that already fits, forwards the reference untouched, and sum(data) becomes indistinguishable from sum(10, 20, 30) inside the method.
When a varargs method competes with a fixed-arity one for the same call, the fixed-arity method wins. Overload resolution makes up to three passes, and the first two consider every candidate as though its signature were fixed, reading Object... as Object[]; only when nothing matches does the third pass start wrapping loose arguments into a new array. So log(String) claims log("hi") and log(String, Object...) never sees it, and a call whose last argument is already an Object[] is passed straight through by the first pass. The same rule produces the classic Object... surprise: String[] is an Object[] and therefore spreads into many elements, while int[] is an object but not an Object[], so it arrives as one element.
public class VarargsSum {
static int sum(int... numbers) {
System.out.print("length " + numbers.length + " -> ");
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
public static void main(String[] args) {
System.out.println(sum());
System.out.println(sum(7));
System.out.println(sum(1, 2, 3, 4));
int[] data = {10, 20, 30};
System.out.println(sum(data));
}
}A varargs parameter is an ordinary array parameter with call-site sugar: the compiler, not the method, builds the array.
Worked examples
Fixed arity wins over varargs
Shows which overload actually runs when a one-argument call could fit both.
public class LogOverloads {
static void log(String message) {
System.out.println("fixed: " + message);
}
static void log(String message, Object... extras) {
System.out.println("varargs: " + message + " (" + extras.length + ")");
}
public static void main(String[] args) {
log("saved");
log("saved", 42);
log("saved", new Object[0]);
}
}Example explained
Line 1log("saved") picks the fixed-arity method because the first two resolution passes ignore varargs and log(String) already fits, so the search stops there.
Line 2log("saved", 42) has no fixed-arity candidate, so the third pass builds new Object[]{Integer.valueOf(42)} and length is 1.
Line 3log("saved", new Object[0]) matches log(String, Object[]) exactly in the first pass, so the empty array is handed over as-is and length is 0, not 1.
Line 4Declaration order in the file is irrelevant; only applicability and specificity decide.
Required parameter first, then forwarding
A separator that is always needed sits in front of the varargs parameter, which is then passed on to another varargs method.
public class Labels {
static String label(String separator, String... parts) {
return "[" + String.join(separator, parts) + "]";
}
public static void main(String[] args) {
System.out.println(label("-", "id", "name", "city"));
System.out.println(label("-"));
String[] fields = {"x", "y"};
System.out.println(label("+", fields));
}
}Example explained
Line 1separator is an ordinary parameter and must precede parts, since the varargs parameter has to be last.
Line 2label("-") compiles because zero trailing arguments are legal; parts arrives as a length-zero String[] and String.join returns an empty string.
Line 3String.join(separator, parts) needs no unpacking: join takes CharSequence..., and String[] is a CharSequence[], so the same array is forwarded rather than re-wrapped.
Line 4label("+", fields) proves the array route and the comma route reach identical code.
What Object... actually receives
Demonstrates when an array argument spreads, when it is wrapped as one element, and when the parameter itself is null.
public class VarargsShapes {
static void show(String label, Object... items) {
System.out.println(label + " -> " + items.length);
}
public static void main(String[] args) {
String[] names = {"ann", "bob"};
int[] scores = {90, 80, 70};
show("String[]", names);
show("int[]", scores);
show("cast", (Object) names);
try {
show("null", (Object[]) null);
} catch (NullPointerException e) {
System.out.println("null -> NullPointerException");
}
}
}Example explained
Line 1show("String[]", names) passes the array through untouched because String[] is a subtype of Object[], so length is 2.
Line 2show("int[]", scores) cannot pass through: int[] is an object but not an Object[], so it is wrapped as a single element and length is 1.
Line 3(Object) names forces the wrapping path for a reference array, the standard trick when you want one element instead of many.
Line 4(Object[]) null sets the parameter itself to null, so items.length throws: an omitted argument list gives an empty array, but a caller can still force null.
Important notes
Every spreading call allocates a new array, which is why the JDK ships fixed-arity companions such as List.of(e1) through List.of(e1..e10) next to List.of(E...) for hot paths.
A generic varargs parameter like T... values creates an array of an erased type, so javac warns about possible heap pollution; @SafeVarargs is only honest if the method never stores into that array and never lets it escape.
Common mistakes
Writing the varargs parameter anywhere but last, as in void report(String... lines, boolean verbose), which fails to compile with "varargs parameter must be the last parameter" because the compiler would have no way to know where the collected group ends.
Reading parts[0] without checking parts.length; the zero-argument call compiles happily and then throws ArrayIndexOutOfBoundsException at runtime, so the bug only appears for the caller who omits the optional arguments.
Calling show(null) on a show(Object...) method expecting a one-element array holding null; the null becomes the whole array instead, and the first .length or for-each throws NullPointerException. Write show((Object) null) when you mean one null element.
Try it yourself
Change, predict, then run
Write static double average(double... values) that returns 0.0 when no arguments are passed, then print average(1, 2, 3, 4), average(), and average(new double[]{2.5, 3.5}) to see the three call shapes reach the same body.
Open the Java workspaceCheck your understanding
A class declares both void log(String m) and void log(String m, Object... rest). What happens for the call log("hi") and why?
- The varargs version runs with an empty array, since it can legally accept a single String argument.
- The fixed-arity log(String) runs, because varargs is only considered after no fixed-arity candidate fits.
- It does not compile: both methods are applicable to one String argument, so the call is ambiguous.
- Whichever of the two methods is declared first in the source file runs.
Show answer
Overload resolution makes up to three ordered passes, and the first two treat every candidate as fixed-arity, so log(String) is applicable immediately and the search stops before varargs is ever considered. Option 0 is tempting because log(String, Object...) genuinely can accept one String, but being callable is not the same as being chosen; ambiguity never arises either, because the passes are tried in order rather than weighed against each other.