Java Varargs
Varargs (short for “variable-length arguments”) let you write a Java method that accepts zero, one, or many arguments of the same type without writing multiple overloaded methods. Instead of forcing every caller to pass an array explicitly, you write the method once with a special type... name parameter, and Java quietly packages whatever arguments are passed into an array for you. Varargs are used everywhere in the standard library — String.format, List.of, and Arrays.asList are all varargs methods — so understanding them well is essential for reading and writing idiomatic Java.
Overview / How It Works
A varargs parameter is declared with three dots (...) after the type, for example int... numbers. Inside the method body, that parameter behaves exactly like an ordinary array — you can use its length field, loop over it with a for-each loop, or index into it. The magic is entirely on the calling side: the compiler lets the caller pass a comma-separated list of values instead of building an array by hand.
Under the hood, varargs are not a new language feature at the bytecode level — they are syntactic sugar over arrays. When you compile a varargs method, javac marks it with the ACC_VARARGS flag in the class file, and the parameter’s actual type is an array type (int[] for int...). At every call site, the compiler generates code that creates a new array, fills it with the supplied arguments, and passes that array as the last argument — unless the caller already passed an array directly, in which case no wrapping happens and that array is used as-is. This means a varargs method and an equivalent array-parameter method are interchangeable from the JVM’s point of view; only the calling syntax differs.
Because varargs desugar to arrays, a few consequences follow: exactly one array object is allocated per call (a minor performance cost in extremely hot loops), the parameter can hold primitives or objects, and a varargs parameter can receive zero arguments, in which case it becomes an array of length zero — never null (unless the caller explicitly passes null).
Syntax
returnType methodName(Type... parameterName) {
// parameterName behaves like Type[] here
}
| Part | Meaning |
|---|---|
Type... |
The element type followed by three dots, marking this parameter as variable-length. |
parameterName |
Used inside the method exactly like an array of Type. |
| Position | A varargs parameter must be the last parameter in the method’s parameter list, and a method can have only one varargs parameter. |
| Fixed parameters | You can mix ordinary parameters before the varargs one, e.g. void log(String tag, String... messages). |
At a call site you may pass individual values separated by commas, pass an existing array of the matching type, or pass nothing at all if only the varargs parameter remains unfilled.
Examples
Example 1: A basic sum method
public class Main {
public static void main(String[] args) {
System.out.println(sum(1, 2, 3));
System.out.println(sum(10, 20));
System.out.println(sum());
}
static int sum(int... numbers) {
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
}
Output:
6
30
0
The sum method is called with three different argument counts — three, two, and zero — and it works for all of them because numbers is really just an int[] being built for each call. When no arguments are passed, numbers is an empty array (length == 0), so the loop body never executes and the total stays 0.
Example 2: Mixing a fixed parameter with varargs
public class Main {
public static void main(String[] args) {
printReport("Sales", "Jan", "Feb", "Mar");
printReport("Empty Report");
}
static void printReport(String title, String... months) {
System.out.println("Report: " + title);
System.out.println("Months included: " + months.length);
for (String m : months) {
System.out.println(" - " + m);
}
}
}
Output:
Report: Sales
Months included: 3
- Jan
- Feb
- Mar
Report: Empty Report
Months included: 0
Here title is a required fixed parameter, and months soaks up everything after it. Because the varargs parameter must come last, the compiler can always tell where the fixed arguments end and the variable-length list begins.
Example 3: Passing an existing array directly
public class Main {
public static void main(String[] args) {
int[] data = {5, 10, 15};
System.out.println(average(data));
System.out.println(average(2, 4, 6, 8));
}
static double average(int... values) {
if (values.length == 0) {
return 0;
}
int total = 0;
for (int v : values) {
total += v;
}
return (double) total / values.length;
}
}
Output:
10.0
5.0
Because values is really an int[], you can pass an already-built array (data) straight in without the compiler wrapping it again, or you can pass loose comma-separated values and let the compiler build the array for you. Both call styles reach the exact same method.
How It Works Step by Step (Under the Hood)
- The compiler sees a call like
sum(1, 2, 3)matching a method declared assum(int... numbers). - Since the arguments are loose values rather than a single matching array, javac generates bytecode that allocates a new array (
new int[3]), stores1,2, and3into it, and passes that array reference as the method’s single argument. - Inside
sum, the parameternumbersis, at the bytecode level, indistinguishable from a normalint[]parameter — the for-each loop compiles to the same array-iteration bytecode either way. - If instead you call
sum(data)wheredatais already anint[], the compiler recognizes the type already matches and skips the wrapping step, passingdataby reference with no extra allocation. - Overload resolution happens before varargs are considered: the compiler first looks for an exact-arity match among non-varargs methods, and only falls back to a varargs method if no fixed-arity method fits. This is why having both
foo(int, int)andfoo(int...)and callingfoo(1, 2)always picks the fixed-arity version.
Common Mistakes
Mistake 1: Varargs parameter not last
Varargs must be the final parameter, because the compiler needs to know unambiguously where the fixed parameters stop.
// Will NOT compile: varargs parameter must be the last one
static void bad(int... nums, String label) {
System.out.println(label);
}
The fix is simply to reorder the parameters so the varargs one comes last:
static void good(String label, int... nums) {
System.out.println(label);
}
Mistake 2: Passing a primitive array to an Object… varargs parameter
This one compiles fine but produces a surprising result, because an int[] is not an Object[] — primitives don’t autobox into arrays, only into individual values. When you pass a primitive array where Object... is expected, Java treats the whole array as a single object, not as the list of elements to unpack.
public class Main {
public static void main(String[] args) {
int[] nums = {1, 2, 3};
printCount(nums);
printCount(1, 2, 3);
}
static void printCount(Object... items) {
System.out.println("Count: " + items.length);
}
}
Output:
Count: 1
Count: 3
The first call reports a count of 1 because the entire int[] is boxed as one Object element, not three. The second call, using loose int literals, correctly autoboxes each value into an Integer and reports 3. To avoid this trap, either change the varargs type to a matching wrapper type such as Integer..., or box the elements yourself (for example with Arrays.stream(nums).boxed().toArray()) before passing them.
Best Practices
- Use varargs for genuinely variable-length data, such as formatting strings, building collections, or logging — not as a substitute for a well-defined fixed parameter list.
- Put required, always-present parameters before the varargs parameter, and keep the varargs one last, as the language requires.
- Only declare one varargs parameter per method; you cannot have more than one.
- Avoid overloading a varargs method with a fixed-arity method of the exact same name and compatible types unless you fully understand Java’s overload-resolution order, since it can create confusing, hard-to-predict call behavior.
- Be careful mixing primitive arrays with
Object...or wrapper-type varargs parameters — box explicitly rather than relying on autoboxing of the whole array. - Remember that each varargs call (with loose arguments) allocates a new array; avoid varargs methods in extremely performance-sensitive tight loops if that allocation matters.
- Document what an empty varargs call means (e.g., “no filters applied”) since callers can always pass zero arguments.
Practice Exercises
- Exercise 1: Write a method
max(int... values)that returns the largest value passed in, and returnsInteger.MIN_VALUEif no arguments are given. Test it withmax(3, 7, 2)andmax(). - Exercise 2: Write a method
joinWords(String separator, String... words)that joins the words into a single string using the given separator (for example,joinWords("-", "a", "b", "c")should produce"a-b-c"). Handle the case of zero words by returning an empty string. - Exercise 3: Write a method
describe(String label, Object... details)that prints the label followed by the number of details and each detail on its own line. Call it once with three mixed-type arguments (aString, anint, and aboolean) and once with none, and predict the output before running it.
Summary
- A varargs parameter is written as
Type... nameand lets a method accept zero or more arguments of that type. - Varargs are syntactic sugar over arrays — inside the method, the parameter is a real array with a
lengthfield and full array behavior. - A varargs parameter must be the last parameter in the method signature, and a method can have only one.
- Callers can pass loose comma-separated values or an already-built matching array; the compiler only allocates a new array when wrapping loose values.
- Fixed-arity overloads are preferred over varargs overloads during method resolution.
- Watch out for passing a primitive array into an
Object...(or wrapper-type) varargs parameter — it becomes a single element instead of being unpacked.
